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 new instance.web.Dialog(this, {
591 title:result.warning.title,
593 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
595 }, QWeb.render("CrashManager.warning", result.warning)).open();
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(e) {
735 $(e.target).attr("disabled", true);
736 return this.save().done(function(result) {
737 self.trigger("save", result);
738 self.reload().then(function() {
740 var parent = self.ViewManager.ActionManager.getParent();
742 parent.menu.do_reload_needaction();
745 }).always(function(){
746 $(e.target).attr("disabled", false);
749 on_button_cancel: function(event) {
750 if (this.can_be_discarded()) {
751 if (this.get('actual_mode') === 'create') {
752 this.trigger('history_back');
755 this.trigger('load_record', this.datarecord);
758 this.trigger('on_button_cancel');
761 on_button_new: function() {
764 return $.when(this.has_been_loaded).then(function() {
765 if (self.can_be_discarded()) {
766 return self.load_defaults();
770 on_button_edit: function() {
771 return this.to_edit_mode();
773 on_button_create: function() {
774 this.dataset.index = null;
777 on_button_duplicate: function() {
779 return this.has_been_loaded.then(function() {
780 return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
781 self.record_created(new_id);
786 on_button_delete: function() {
788 var def = $.Deferred();
789 this.has_been_loaded.done(function() {
790 if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
791 self.dataset.unlink([self.datarecord.id]).done(function() {
792 if (self.dataset.size()) {
793 self.execute_pager_action('next');
795 self.do_action('history_back');
800 $.async_when().done(function () {
805 return def.promise();
807 can_be_discarded: function() {
808 if (this.$el.is('.oe_form_dirty')) {
809 if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
812 this.$el.removeClass('oe_form_dirty');
817 * Triggers saving the form's record. Chooses between creating a new
818 * record or saving an existing one depending on whether the record
819 * already has an id property.
821 * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
822 * record, should that record be inserted at the start of the dataset (by
823 * default, records are added at the end)
825 save: function(prepend_on_create) {
827 var save_obj = {prepend_on_create: prepend_on_create, ret: null};
828 this.save_list.push(save_obj);
829 return this._process_operations().then(function() {
831 return $.Deferred().reject();
832 return $.when.apply($, save_obj.ret);
834 self.$el.removeClass('oe_form_dirty');
837 _process_save: function(save_obj) {
839 var prepend_on_create = save_obj.prepend_on_create;
841 var form_invalid = false,
843 first_invalid_field = null,
844 readonly_values = {};
845 for (var f in self.fields) {
846 if (!self.fields.hasOwnProperty(f)) { continue; }
850 if (!first_invalid_field) {
851 first_invalid_field = f;
853 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
854 // Special case 'id' field, do not save this field
855 // on 'create' : save all non readonly fields
856 // on 'edit' : save non readonly modified fields
857 if (!f.get("readonly")) {
858 values[f.name] = f.get_value();
860 readonly_values[f.name] = f.get_value();
865 self.set({'display_invalid_fields': true});
866 first_invalid_field.focus();
868 return $.Deferred().reject();
870 self.set({'display_invalid_fields': false});
872 if (!self.datarecord.id) {
874 save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
875 return self.record_created(r, prepend_on_create);
877 } else if (_.isEmpty(values)) {
878 // Not dirty, noop save
879 save_deferral = $.Deferred().resolve({}).promise();
882 save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
883 return self.record_saved(r);
886 return save_deferral;
890 return $.Deferred().reject();
893 on_invalid: function() {
894 var warnings = _(this.fields).chain()
895 .filter(function (f) { return !f.is_valid(); })
897 return _.str.sprintf('<li>%s</li>',
900 warnings.unshift('<ul>');
901 warnings.push('</ul>');
902 this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
905 * Reload the form after saving
907 * @param {Object} r result of the write function.
909 record_saved: function(r) {
910 this.trigger('record_saved', r);
912 // should not happen in the server, but may happen for internal purpose
913 return $.Deferred().reject();
918 * Updates the form' dataset to contain the new record:
920 * * Adds the newly created record to the current dataset (at the end by
922 * * Selects that record (sets the dataset's index to point to the new
924 * * Updates the pager and sidebar displays
927 * @param {Boolean} [prepend_on_create=false] adds the newly created record
928 * at the beginning of the dataset instead of the end
930 record_created: function(r, prepend_on_create) {
933 // should not happen in the server, but may happen for internal purpose
934 this.trigger('record_created', r);
935 return $.Deferred().reject();
937 this.datarecord.id = r;
938 if (!prepend_on_create) {
939 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
940 this.dataset.index = this.dataset.ids.length - 1;
942 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
943 this.dataset.index = 0;
945 this.do_update_pager();
947 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
949 //openerp.log("The record has been created with id #" + this.datarecord.id);
950 return $.when(this.reload()).then(function () {
951 self.trigger('record_created', r);
952 return _.extend(r, {created: true});
956 on_action: function (action) {
957 console.debug('Executing action', action);
961 return this.reload_mutex.exec(function() {
962 if (self.dataset.index === null || self.dataset.index === undefined) {
963 self.trigger("previous_view");
964 return $.Deferred().reject().promise();
966 if (self.dataset.index < 0) {
967 return $.when(self.on_button_new());
969 var fields = _.keys(self.fields_view.fields);
970 fields.push('display_name');
971 return self.dataset.read_index(fields,
975 'future_display_name': true
977 check_access_rule: true
978 }).then(function(r) {
979 self.trigger('load_record', r);
981 self.do_action('history_back');
986 get_widgets: function() {
987 return _.filter(this.getChildren(), function(obj) {
988 return obj instanceof instance.web.form.FormWidget;
991 get_fields_values: function() {
993 var ids = this.get_selected_ids();
994 values["id"] = ids.length > 0 ? ids[0] : false;
995 _.each(this.fields, function(value_, key) {
996 values[key] = value_.get_value();
1000 get_selected_ids: function() {
1001 var id = this.dataset.ids[this.dataset.index];
1002 return id ? [id] : [];
1004 recursive_save: function() {
1006 return $.when(this.save()).then(function(res) {
1007 if (self.dataset.parent_view)
1008 return self.dataset.parent_view.recursive_save();
1011 recursive_reload: function() {
1014 if (self.dataset.parent_view)
1015 pre = self.dataset.parent_view.recursive_reload();
1016 return pre.then(function() {
1017 return self.reload();
1020 is_dirty: function() {
1021 return _.any(this.fields, function (value_) {
1022 return value_._dirty_flag;
1025 is_interactible_record: function() {
1026 var id = this.datarecord.id;
1028 if (this.options.not_interactible_on_create)
1030 } else if (typeof(id) === "string") {
1031 if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1036 sidebar_eval_context: function () {
1037 return $.when(this.build_eval_context());
1039 open_defaults_dialog: function () {
1041 var display = function (field, value) {
1042 if (!value) { return value; }
1043 if (field instanceof instance.web.form.FieldSelection) {
1044 return _(field.get('values')).find(function (option) {
1045 return option[0] === value;
1047 } else if (field instanceof instance.web.form.FieldMany2One) {
1048 return field.get_displayed();
1052 var fields = _.chain(this.fields)
1053 .map(function (field) {
1054 var value = field.get_value();
1055 // ignore fields which are empty, invisible, readonly, o2m
1058 || field.get('invisible')
1059 || field.get("readonly")
1060 || field.field.type === 'one2many'
1061 || field.field.type === 'many2many'
1062 || field.field.type === 'binary'
1063 || field.password) {
1069 string: field.string,
1071 displayed: display(field, value),
1075 .sortBy(function (field) { return field.string; })
1077 var conditions = _.chain(self.fields)
1078 .filter(function (field) { return field.field.change_default; })
1079 .map(function (field) {
1080 var value = field.get_value();
1083 string: field.string,
1085 displayed: display(field, value),
1089 var d = new instance.web.Dialog(this, {
1090 title: _t("Set Default"),
1093 conditions: conditions
1096 {text: _t("Close"), click: function () { d.close(); }},
1097 {text: _t("Save default"), click: function () {
1098 var $defaults = d.$el.find('#formview_default_fields');
1099 var field_to_set = $defaults.val();
1100 if (!field_to_set) {
1101 $defaults.parent().addClass('oe_form_invalid');
1104 var condition = d.$el.find('#formview_default_conditions').val(),
1105 all_users = d.$el.find('#formview_default_all').is(':checked');
1106 new instance.web.DataSet(self, 'ir.values').call(
1110 self.fields[field_to_set].get_value(),
1114 ]).done(function () { d.close(); });
1118 d.template = 'FormView.set_default';
1121 register_field: function(field, name) {
1122 this.fields[name] = field;
1123 this.fields_order.push(name);
1124 if (JSON.parse(field.node.attrs.default_focus || "0")) {
1125 this.default_focus_field = field;
1128 field.on('focused', null, this.proxy('widgetFocused'))
1129 .on('blurred', null, this.proxy('widgetBlurred'));
1130 if (this.get_field_desc(name).translate) {
1131 this.translatable_fields.push(field);
1133 field.on('changed_value', this, function() {
1134 if (field.is_syntax_valid()) {
1135 this.trigger('field_changed:' + name);
1137 if (field._inhibit_on_change_flag) {
1140 field._dirty_flag = true;
1141 if (field.is_syntax_valid()) {
1142 this.do_onchange(field);
1143 this.on_form_changed(true);
1144 this.do_notify_change();
1148 get_field_desc: function(field_name) {
1149 return this.fields_view.fields[field_name];
1151 get_field_value: function(field_name) {
1152 return this.fields[field_name].get_value();
1154 compute_domain: function(expression) {
1155 return instance.web.form.compute_domain(expression, this.fields);
1157 _build_view_fields_values: function() {
1158 var a_dataset = this.dataset;
1159 var fields_values = this.get_fields_values();
1160 var active_id = a_dataset.ids[a_dataset.index];
1161 _.extend(fields_values, {
1162 active_id: active_id || false,
1163 active_ids: active_id ? [active_id] : [],
1164 active_model: a_dataset.model,
1167 if (a_dataset.parent_view) {
1168 fields_values.parent = a_dataset.parent_view.get_fields_values();
1170 return fields_values;
1172 build_eval_context: function() {
1173 var a_dataset = this.dataset;
1174 return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1179 * Interface to be implemented by rendering engines for the form view.
1181 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1182 set_fields_view: function(fields_view) {},
1183 set_fields_registry: function(fields_registry) {},
1184 render_to: function($el) {},
1188 * Default rendering engine for the form view.
1190 * It is necessary to set the view using set_view() before usage.
1192 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1193 init: function(view) {
1196 set_fields_view: function(fvg) {
1198 this.version = parseFloat(this.fvg.arch.attrs.version);
1199 if (isNaN(this.version)) {
1203 set_tags_registry: function(tags_registry) {
1204 this.tags_registry = tags_registry;
1206 set_fields_registry: function(fields_registry) {
1207 this.fields_registry = fields_registry;
1209 set_widgets_registry: function(widgets_registry) {
1210 this.widgets_registry = widgets_registry;
1212 // Backward compatibility tools, current default version: v6.1
1213 process_version: function() {
1214 if (this.version < 7.0) {
1215 this.$form.find('form:first').wrapInner('<group col="4"/>');
1216 this.$form.find('page').each(function() {
1217 if (!$(this).parents('field').length) {
1218 $(this).wrapInner('<group col="4"/>');
1223 get_arch_fragment: function() {
1224 var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1225 // IE won't allow custom button@type and will revert it to spec default : 'submit'
1226 $('button', doc).each(function() {
1227 $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1229 // IE's html parser is also a css parser. How convenient...
1230 $('board', doc).each(function() {
1231 $(this).attr('layout', $(this).attr('style'));
1233 return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1235 render_to: function($target) {
1237 this.$target = $target;
1239 this.$form = this.get_arch_fragment();
1241 this.process_version();
1243 this.fields_to_init = [];
1244 this.tags_to_init = [];
1245 this.widgets_to_init = [];
1247 this.process(this.$form);
1249 this.$form.appendTo(this.$target);
1251 this.to_replace = [];
1253 _.each(this.fields_to_init, function($elem) {
1254 var name = $elem.attr("name");
1255 if (!self.fvg.fields[name]) {
1256 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1258 var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1260 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1262 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1263 var $label = self.labels[$elem.attr("name")];
1265 w.set_input_id($label.attr("for"));
1267 self.alter_field(w);
1268 self.view.register_field(w, $elem.attr("name"));
1269 self.to_replace.push([w, $elem]);
1271 _.each(this.tags_to_init, function($elem) {
1272 var tag_name = $elem[0].tagName.toLowerCase();
1273 var obj = self.tags_registry.get_object(tag_name);
1274 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1275 self.to_replace.push([w, $elem]);
1277 _.each(this.widgets_to_init, function($elem) {
1278 var widget_type = $elem.attr("type");
1279 var obj = self.widgets_registry.get_object(widget_type);
1280 var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1281 self.to_replace.push([w, $elem]);
1284 init_fields: function() {
1286 _.each(this.to_replace, function(el) {
1287 defs.push(el[0].replace(el[1]));
1288 if (el[1].children().length) {
1289 el[0].$el.append(el[1].children());
1292 this.to_replace = [];
1293 return $.when.apply($, defs);
1295 render_element: function(template /* dictionaries */) {
1296 var dicts = [].slice.call(arguments).slice(1);
1297 var dict = _.extend.apply(_, dicts);
1298 dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1299 return $(QWeb.render(template, dict));
1301 alter_field: function(field) {
1303 toggle_layout_debugging: function() {
1304 if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1305 this.$target.find('[title]').removeAttr('title');
1306 this.$target.find('.oe_form_group_cell').each(function() {
1307 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1308 $(this).attr('title', text);
1311 this.$target.toggleClass('oe_layout_debugging');
1313 process: function($tag) {
1315 var tagname = $tag[0].nodeName.toLowerCase();
1316 if (this.tags_registry.contains(tagname)) {
1317 this.tags_to_init.push($tag);
1318 return (tagname === 'button') ? this.process_button($tag) : $tag;
1320 var fn = self['process_' + tagname];
1322 var args = [].slice.call(arguments);
1324 return fn.apply(self, args);
1326 // generic tag handling, just process children
1327 $tag.children().each(function() {
1328 self.process($(this));
1330 self.handle_common_properties($tag, $tag);
1331 $tag.removeAttr("modifiers");
1335 process_button: function ($button) {
1337 $button.children().each(function() {
1338 self.process($(this));
1342 process_widget: function($widget) {
1343 this.widgets_to_init.push($widget);
1346 process_sheet: function($sheet) {
1347 var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1348 this.handle_common_properties($new_sheet, $sheet);
1349 var $dst = $new_sheet.find('.oe_form_sheet');
1350 $sheet.contents().appendTo($dst);
1351 $sheet.before($new_sheet).remove();
1352 this.process($new_sheet);
1354 process_form: function($form) {
1355 if ($form.find('> sheet').length === 0) {
1356 $form.addClass('oe_form_nosheet');
1358 var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1359 this.handle_common_properties($new_form, $form);
1360 $form.contents().appendTo($new_form);
1361 if ($form[0] === this.$form[0]) {
1362 // If root element, replace it
1363 this.$form = $new_form;
1365 $form.before($new_form).remove();
1367 this.process($new_form);
1370 * Used by direct <field> children of a <group> tag only
1371 * This method will add the implicit <label...> for every field
1374 preprocess_field: function($field) {
1376 var name = $field.attr('name'),
1377 field_colspan = parseInt($field.attr('colspan'), 10),
1378 field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1380 if ($field.attr('nolabel') === '1')
1382 $field.attr('nolabel', '1');
1384 this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1385 $(el).parents().each(function(unused, tag) {
1386 var name = tag.tagName.toLowerCase();
1387 if (name === "field" || name in self.tags_registry.map)
1394 var $label = $('<label/>').attr({
1396 "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1397 "string": $field.attr('string'),
1398 "help": $field.attr('help'),
1399 "class": $field.attr('class'),
1401 $label.insertBefore($field);
1402 if (field_colspan > 1) {
1403 $field.attr('colspan', field_colspan - 1);
1407 process_field: function($field) {
1408 if ($field.parent().is('group')) {
1409 // No implicit labels for normal fields, only for <group> direct children
1410 var $label = this.preprocess_field($field);
1412 this.process($label);
1415 this.fields_to_init.push($field);
1418 process_group: function($group) {
1420 $group.children('field').each(function() {
1421 self.preprocess_field($(this));
1423 var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1425 if ($new_group.first().is('table.oe_form_group')) {
1426 $table = $new_group;
1427 } else if ($new_group.filter('table.oe_form_group').length) {
1428 $table = $new_group.filter('table.oe_form_group').first();
1430 $table = $new_group.find('table.oe_form_group').first();
1434 cols = parseInt($group.attr('col') || 2, 10),
1438 $group.children().each(function(a,b,c) {
1439 var $child = $(this);
1440 var colspan = parseInt($child.attr('colspan') || 1, 10);
1441 var tagName = $child[0].tagName.toLowerCase();
1442 var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1443 var newline = tagName === 'newline';
1445 // Note FME: those classes are used in layout debug mode
1446 if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1447 $tr.addClass('oe_form_group_row_incomplete');
1449 $tr.addClass('oe_form_group_row_newline');
1456 if (!$tr || row_cols < colspan) {
1457 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1459 } else if (tagName==='group') {
1460 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1461 $td.addClass('oe_group_right');
1463 row_cols -= colspan;
1465 // invisibility transfer
1466 var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1467 var invisible = field_modifiers.invisible;
1468 self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1470 $tr.append($td.append($child));
1471 children.push($child[0]);
1473 if (row_cols && $td) {
1474 $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1476 $group.before($new_group).remove();
1478 $table.find('> tbody > tr').each(function() {
1479 var to_compute = [],
1482 $(this).children().each(function() {
1484 $child = $td.children(':first');
1485 if ($child.attr('cell-class')) {
1486 $td.addClass($child.attr('cell-class'));
1488 switch ($child[0].tagName.toLowerCase()) {
1492 if ($child.attr('for')) {
1493 $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1494 row_cols-= $td.attr('colspan') || 1;
1499 var width = _.str.trim($child.attr('width') || ''),
1500 iwidth = parseInt(width, 10);
1502 if (width.substr(-1) === '%') {
1504 width = iwidth + '%';
1507 $td.css('min-width', width + 'px');
1509 $td.attr('width', width);
1510 $child.removeAttr('width');
1511 row_cols-= $td.attr('colspan') || 1;
1513 to_compute.push($td);
1519 var unit = Math.floor(total / row_cols);
1520 if (!$(this).is('.oe_form_group_row_incomplete')) {
1521 _.each(to_compute, function($td, i) {
1522 var width = parseInt($td.attr('colspan'), 10) * unit;
1523 $td.attr('width', width + '%');
1529 _.each(children, function(el) {
1530 self.process($(el));
1532 this.handle_common_properties($new_group, $group);
1535 process_notebook: function($notebook) {
1538 $notebook.find('> page').each(function() {
1539 var $page = $(this);
1540 var page_attrs = $page.getAttributes();
1541 page_attrs.id = _.uniqueId('notebook_page_');
1542 var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1543 $page.contents().appendTo($new_page);
1544 $page.before($new_page).remove();
1545 var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1546 page_attrs.__page = $new_page;
1547 page_attrs.__ic = ic;
1548 pages.push(page_attrs);
1550 $new_page.children().each(function() {
1551 self.process($(this));
1554 var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1555 $notebook.contents().appendTo($new_notebook);
1556 $notebook.before($new_notebook).remove();
1557 self.process($($new_notebook.children()[0]));
1558 //tabs and invisibility handling
1559 $new_notebook.tabs();
1560 _.each(pages, function(page, i) {
1563 page.__ic.on("change:effective_invisible", null, function() {
1564 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1565 $new_notebook.tabs('select', i);
1568 var current = $new_notebook.tabs("option", "selected");
1569 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1571 var first_visible = _.find(_.range(pages.length), function(i2) {
1572 return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1574 if (first_visible !== undefined) {
1575 $new_notebook.tabs('select', first_visible);
1580 this.handle_common_properties($new_notebook, $notebook);
1581 return $new_notebook;
1583 process_separator: function($separator) {
1584 var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1585 $separator.before($new_separator).remove();
1586 this.handle_common_properties($new_separator, $separator);
1587 return $new_separator;
1589 process_label: function($label) {
1590 var name = $label.attr("for"),
1591 field_orm = this.fvg.fields[name];
1593 string: $label.attr('string') || (field_orm || {}).string || '',
1594 help: $label.attr('help') || (field_orm || {}).help || '',
1595 _for: name ? _.uniqueId('oe-field-input-') : undefined,
1597 var align = parseFloat(dict.align);
1598 if (isNaN(align) || align === 1) {
1600 } else if (align === 0) {
1606 var $new_label = this.render_element('FormRenderingLabel', dict);
1607 $label.before($new_label).remove();
1608 this.handle_common_properties($new_label, $label);
1610 this.labels[name] = $new_label;
1614 handle_common_properties: function($new_element, $node) {
1615 var str_modifiers = $node.attr("modifiers") || "{}";
1616 var modifiers = JSON.parse(str_modifiers);
1618 if (modifiers.invisible !== undefined)
1619 ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1620 $new_element.addClass($node.attr("class") || "");
1621 $new_element.attr('style', $node.attr('style'));
1622 return {invisibility_changer: ic,};
1629 If you read this documentation, it probably means that you were asked to use a form view widget outside of
1630 a form view. Before going further, you must understand that those fields were never really created for
1631 that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1632 you to hack the system with more style.
1634 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1635 init: function(parent, eval_context) {
1636 this._super(parent);
1637 this.field_descs = {};
1638 this.eval_context = eval_context || {};
1640 display_invalid_fields: false,
1641 actual_mode: 'create',
1644 get_field_desc: function(field_name) {
1645 if (this.field_descs[field_name] === undefined) {
1646 this.field_descs[field_name] = {
1650 return this.field_descs[field_name];
1652 extend_field_desc: function(fields) {
1654 _.each(fields, function(v, k) {
1655 _.extend(self.get_field_desc(k), v);
1658 get_field_value: function(field_name) {
1661 set_values: function(values) {
1664 compute_domain: function(expression) {
1665 return instance.web.form.compute_domain(expression, {});
1667 build_eval_context: function() {
1668 return new instance.web.CompoundContext(this.eval_context);
1672 instance.web.form.compute_domain = function(expr, fields) {
1673 if (! (expr instanceof Array))
1676 for (var i = expr.length - 1; i >= 0; i--) {
1678 if (ex.length == 1) {
1679 var top = stack.pop();
1682 stack.push(stack.pop() || top);
1685 stack.push(stack.pop() && top);
1691 throw new Error(_.str.sprintf(
1692 _t("Unknown operator %s in domain %s"),
1693 ex, JSON.stringify(expr)));
1697 var field = fields[ex[0]];
1699 throw new Error(_.str.sprintf(
1700 _t("Unknown field %s in domain %s"),
1701 ex[0], JSON.stringify(expr)));
1703 var field_value = field.get_value ? field.get_value() : field.value;
1707 switch (op.toLowerCase()) {
1710 stack.push(_.isEqual(field_value, val));
1714 stack.push(!_.isEqual(field_value, val));
1717 stack.push(field_value < val);
1720 stack.push(field_value > val);
1723 stack.push(field_value <= val);
1726 stack.push(field_value >= val);
1729 if (!_.isArray(val)) val = [val];
1730 stack.push(_(val).contains(field_value));
1733 if (!_.isArray(val)) val = [val];
1734 stack.push(!_(val).contains(field_value));
1738 _t("Unsupported operator %s in domain %s"),
1739 op, JSON.stringify(expr));
1742 return _.all(stack, _.identity);
1745 instance.web.form.is_bin_size = function(v) {
1746 return (/^\d+(\.\d*)? \w+$/).test(v);
1750 * Must be applied over an class already possessing the PropertiesMixin.
1752 * Apply the result of the "invisible" domain to this.$el.
1754 instance.web.form.InvisibilityChangerMixin = {
1755 init: function(field_manager, invisible_domain) {
1757 this._ic_field_manager = field_manager;
1758 this._ic_invisible_modifier = invisible_domain;
1759 this._ic_field_manager.on("view_content_has_changed", this, function() {
1760 var result = self._ic_invisible_modifier === undefined ? false :
1761 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1762 self.set({"invisible": result});
1764 this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1765 var check = function() {
1766 if (self.get("invisible") || self.get('force_invisible')) {
1767 self.set({"effective_invisible": true});
1769 self.set({"effective_invisible": false});
1772 this.on('change:invisible', this, check);
1773 this.on('change:force_invisible', this, check);
1777 this.on("change:effective_invisible", this, this._check_visibility);
1778 this._check_visibility();
1780 _check_visibility: function() {
1781 this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1785 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1786 init: function(parent, field_manager, invisible_domain, $el) {
1787 this.setParent(parent);
1788 instance.web.PropertiesMixin.init.call(this);
1789 instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1796 Base class for all fields, custom widgets and buttons to be displayed in the form view.
1799 - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1800 the values of the "readonly" property and the "mode" property on the field manager.
1802 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1804 * @constructs instance.web.form.FormWidget
1805 * @extends instance.web.Widget
1807 * @param field_manager
1810 init: function(field_manager, node) {
1811 this._super(field_manager);
1812 this.field_manager = field_manager;
1813 if (this.field_manager instanceof instance.web.FormView)
1814 this.view = this.field_manager;
1816 this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1817 instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1819 this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1825 // some events to make the property "effective_readonly" sync automatically with "readonly" and
1826 // "mode" on field_manager
1828 var test_effective_readonly = function() {
1829 self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1831 this.on("change:readonly", this, test_effective_readonly);
1832 this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1833 test_effective_readonly.call(this);
1835 renderElement: function() {
1836 this.process_modifiers();
1838 this.$el.addClass(this.node.attrs["class"] || "");
1840 destroy: function() {
1842 this._super.apply(this, arguments);
1845 * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1847 * This method is an utility method that is meant to be called by child classes.
1849 * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1851 setupFocus: function ($e) {
1854 focus: function () { self.trigger('focused'); },
1855 blur: function () { self.trigger('blurred'); }
1858 process_modifiers: function() {
1860 for (var a in this.modifiers) {
1861 if (!this.modifiers.hasOwnProperty(a)) { continue; }
1862 if (!_.include(["invisible"], a)) {
1863 var val = this.field_manager.compute_domain(this.modifiers[a]);
1869 do_attach_tooltip: function(widget, trigger, options) {
1870 widget = widget || this;
1871 trigger = trigger || this.$el;
1872 options = _.extend({
1877 var template = widget.template + '.tooltip';
1878 if (!QWeb.has_template(template)) {
1879 template = 'WidgetLabel.tooltip';
1881 return QWeb.render(template, {
1882 debug: instance.session.debug,
1886 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1891 $(trigger).tipsy(options);
1894 * Builds a new context usable for operations related to fields by merging
1895 * the fields'context with the action's context.
1897 build_context: function() {
1898 // only use the model's context if there is not context on the node
1899 var v_context = this.node.attrs.context;
1901 v_context = (this.field || {}).context || {};
1904 if (v_context.__ref || true) { //TODO: remove true
1905 var fields_values = this.field_manager.build_eval_context();
1906 v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1910 build_domain: function() {
1911 var f_domain = this.field.domain || [];
1912 var n_domain = this.node.attrs.domain || null;
1913 // if there is a domain on the node, overrides the model's domain
1914 var final_domain = n_domain !== null ? n_domain : f_domain;
1915 if (!(final_domain instanceof Array) || true) { //TODO: remove true
1916 var fields_values = this.field_manager.build_eval_context();
1917 final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1919 return final_domain;
1923 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1924 template: 'WidgetButton',
1925 init: function(field_manager, node) {
1926 node.attrs.type = node.attrs['data-button-type'];
1927 this.is_stat_button = /\boe_stat_button\b/.test(node.attrs['class']);
1928 this.icon = node.attrs.icon && "<span class=\"fa " + node.attrs.icon + " fa-fw\"></span>";
1929 this._super(field_manager, node);
1930 this.force_disabled = false;
1931 this.string = (this.node.attrs.string || '').replace(/_/g, '');
1932 if (JSON.parse(this.node.attrs.default_focus || "0")) {
1933 // TODO fme: provide enter key binding to widgets
1934 this.view.default_focus_button = this;
1936 if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1937 this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1941 this._super.apply(this, arguments);
1942 this.view.on('view_content_has_changed', this, this.check_disable);
1943 this.check_disable();
1944 this.$el.click(this.on_click);
1945 if (this.node.attrs.help || instance.session.debug) {
1946 this.do_attach_tooltip();
1948 this.setupFocus(this.$el);
1950 on_click: function() {
1952 this.force_disabled = true;
1953 this.check_disable();
1954 this.execute_action().always(function() {
1955 self.force_disabled = false;
1956 self.check_disable();
1959 execute_action: function() {
1961 var exec_action = function() {
1962 if (self.node.attrs.confirm) {
1963 var def = $.Deferred();
1964 var dialog = new instance.web.Dialog(this, {
1965 title: _t('Confirm'),
1967 {text: _t("Cancel"), click: function() {
1968 this.parents('.modal').modal('hide');
1971 {text: _t("Ok"), click: function() {
1973 self.on_confirmed().always(function() {
1974 self2.parents('.modal').modal('hide');
1979 }, $('<div/>').text(self.node.attrs.confirm)).open();
1980 dialog.on("closing", null, function() {def.resolve();});
1981 return def.promise();
1983 return self.on_confirmed();
1986 if (!this.node.attrs.special) {
1987 return this.view.recursive_save().then(exec_action);
1989 return exec_action();
1992 on_confirmed: function() {
1995 var context = this.build_context();
1996 return this.view.do_execute_action(
1997 _.extend({}, this.node.attrs, {context: context}),
1998 this.view.dataset, this.view.datarecord.id, function (reason) {
1999 if (!_.isObject(reason)) {
2000 self.view.recursive_reload();
2004 check_disable: function() {
2005 var disabled = (this.force_disabled || !this.view.is_interactible_record());
2006 this.$el.prop('disabled', disabled);
2007 this.$el.css('color', disabled ? 'grey' : '');
2012 * Interface to be implemented by fields.
2015 * - changed_value: triggered when the value of the field has changed. This can be due
2016 * to a user interaction or a call to set_value().
2019 instance.web.form.FieldInterface = {
2021 * Constructor takes 2 arguments:
2022 * - field_manager: Implements FieldManagerMixin
2023 * - node: the "<field>" node in json form
2025 init: function(field_manager, node) {},
2027 * Called by the form view to indicate the value of the field.
2029 * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2030 * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2031 * before the widget is inserted into the DOM.
2033 * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2034 * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2035 * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2036 * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2037 * no information is ever given to know which format is used.
2039 set_value: function(value_) {},
2041 * Get the current value of the widget.
2043 * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2044 * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2045 * For example if the field is marked as "required", a call to get_value() can return false.
2047 * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2048 * return a default value according to the type of field.
2050 * This method is always assumed to perform synchronously, it can not return a promise.
2052 * If there was no user interaction to modify the value of the field, it is always assumed that
2053 * get_value() return the same semantic value than the one passed in the last call to set_value(),
2054 * although the syntax can be different. This can be the case for type of fields that have a different
2055 * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2057 get_value: function() {},
2059 * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2062 set_input_id: function(id) {},
2064 * Returns true if is_syntax_valid() returns true and the value is semantically
2065 * valid too according to the semantic restrictions applied to the field.
2067 is_valid: function() {},
2069 * Returns true if the field holds a value which is syntactically correct, ignoring
2070 * the potential semantic restrictions applied to the field.
2072 is_syntax_valid: function() {},
2074 * Must set the focus on the field. Return false if field is not focusable.
2076 focus: function() {},
2078 * Called when the translate button is clicked.
2080 on_translate: function() {},
2082 This method is called by the form view before reading on_change values and before saving. It tells
2083 the field to save its value before reading it using get_value(). Must return a promise.
2085 commit_value: function() {},
2089 * Abstract class for classes implementing FieldInterface.
2092 * - value: useful property to hold the value of the field. By default, set_value() and get_value()
2093 * set and retrieve the value property. Changing the value property also triggers automatically
2094 * a 'changed_value' event that inform the view to trigger on_changes.
2097 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2099 * @constructs instance.web.form.AbstractField
2100 * @extends instance.web.form.FormWidget
2102 * @param field_manager
2105 init: function(field_manager, node) {
2107 this._super(field_manager, node);
2108 this.name = this.node.attrs.name;
2109 this.field = this.field_manager.get_field_desc(this.name);
2110 this.widget = this.node.attrs.widget;
2111 this.string = this.node.attrs.string || this.field.string || this.name;
2112 this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2113 this.set({'value': false});
2115 this.on("change:value", this, function() {
2116 this.trigger('changed_value');
2117 this._check_css_flags();
2120 renderElement: function() {
2123 if (this.field.translate && this.view) {
2124 this.$el.addClass('oe_form_field_translatable');
2125 this.$el.find('.oe_field_translate').click(this.on_translate);
2127 this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2128 if (instance.session.debug) {
2129 this.do_attach_tooltip(this, this.$label[0] || this.$el);
2130 this.$label.off('dblclick').on('dblclick', function() {
2131 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2133 console.log("window.w =", window.w);
2136 if (!this.disable_utility_classes) {
2137 this.off("change:required", this, this._set_required);
2138 this.on("change:required", this, this._set_required);
2139 this._set_required();
2141 this._check_visibility();
2142 this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2143 this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2144 this._check_css_flags();
2147 var tmp = this._super();
2148 this.on("change:value", this, function() {
2149 if (! this.no_rerender)
2150 this.render_value();
2152 this.render_value();
2155 * Private. Do not use.
2157 _set_required: function() {
2158 this.$el.toggleClass('oe_form_required', this.get("required"));
2160 set_value: function(value_) {
2161 this.set({'value': value_});
2163 get_value: function() {
2164 return this.get('value');
2167 Utility method that all implementations should use to change the
2168 value without triggering a re-rendering.
2170 internal_set_value: function(value_) {
2171 var tmp = this.no_rerender;
2172 this.no_rerender = true;
2173 this.set({'value': value_});
2174 this.no_rerender = tmp;
2177 This method is called each time the value is modified.
2179 render_value: function() {},
2180 is_valid: function() {
2181 return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2183 is_syntax_valid: function() {
2187 * Method useful to implement to ease validity testing. Must return true if the current
2188 * value is similar to false in OpenERP.
2190 is_false: function() {
2191 return this.get('value') === false;
2193 _check_css_flags: function() {
2194 if (this.field.translate) {
2195 this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2197 if (!this.disable_utility_classes) {
2198 if (this.field_manager.get('display_invalid_fields')) {
2199 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2206 set_input_id: function(id) {
2207 this.id_for_label = id;
2209 on_translate: function() {
2211 var trans = new instance.web.DataSet(this, 'ir.translation');
2212 return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2217 set_dimensions: function (height, width) {
2223 commit_value: function() {
2229 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2232 instance.web.form.ReinitializeWidgetMixin = {
2234 * Default implementation of, you should not override it, use initialize_field() instead.
2237 this.initialize_field();
2240 initialize_field: function() {
2241 this.on("change:effective_readonly", this, this.reinitialize);
2242 this.initialize_content();
2244 reinitialize: function() {
2245 this.destroy_content();
2246 this.renderElement();
2247 this.initialize_content();
2250 * Called to destroy anything that could have been created previously, called before a
2251 * re-initialization.
2253 destroy_content: function() {},
2255 * Called to initialize the content.
2257 initialize_content: function() {},
2261 * A mixin to apply on any field that has to completely re-render when its readonly state
2264 instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2265 reinitialize: function() {
2266 instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2267 this.render_value();
2272 Some hack to make placeholders work in ie9.
2274 if (!('placeholder' in document.createElement('input'))) {
2275 document.addEventListener("DOMNodeInserted",function(event){
2276 var nodename = event.target.nodeName.toLowerCase();
2277 if ( nodename === "input" || nodename == "textarea" ) {
2278 $(event.target).placeholder();
2283 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2284 template: 'FieldChar',
2285 widget_class: 'oe_form_field_char',
2287 'change input': 'store_dom_value',
2289 init: function (field_manager, node) {
2290 this._super(field_manager, node);
2291 this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2293 initialize_content: function() {
2294 this.setupFocus(this.$('input'));
2296 store_dom_value: function () {
2297 if (!this.get('effective_readonly')
2298 && this.$('input').length
2299 && this.is_syntax_valid()) {
2300 this.internal_set_value(
2302 this.$('input').val()));
2305 commit_value: function () {
2306 this.store_dom_value();
2307 return this._super();
2309 render_value: function() {
2310 var show_value = this.format_value(this.get('value'), '');
2311 if (!this.get("effective_readonly")) {
2312 this.$el.find('input').val(show_value);
2314 if (this.password) {
2315 show_value = new Array(show_value.length + 1).join('*');
2317 this.$(".oe_form_char_content").text(show_value);
2320 is_syntax_valid: function() {
2321 if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2323 this.parse_value(this.$('input').val(), '');
2331 parse_value: function(val, def) {
2332 return instance.web.parse_value(val, this, def);
2334 format_value: function(val, def) {
2335 return instance.web.format_value(val, this, def);
2337 is_false: function() {
2338 return this.get('value') === '' || this._super();
2341 var input = this.$('input:first')[0];
2342 return input ? input.focus() : false;
2344 set_dimensions: function (height, width) {
2345 this._super(height, width);
2346 this.$('input').css({
2353 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2354 process_modifiers: function () {
2356 this.set({ readonly: true });
2360 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2361 template: 'FieldEmail',
2362 initialize_content: function() {
2364 var $button = this.$el.find('button');
2365 $button.click(this.on_button_clicked);
2366 this.setupFocus($button);
2368 render_value: function() {
2369 if (!this.get("effective_readonly")) {
2373 .attr('href', 'mailto:' + this.get('value'))
2374 .text(this.get('value') || '');
2377 on_button_clicked: function() {
2378 if (!this.get('value') || !this.is_syntax_valid()) {
2379 this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2381 location.href = 'mailto:' + this.get('value');
2386 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2387 template: 'FieldUrl',
2388 initialize_content: function() {
2390 var $button = this.$el.find('button');
2391 $button.click(this.on_button_clicked);
2392 this.setupFocus($button);
2394 render_value: function() {
2395 if (!this.get("effective_readonly")) {
2398 var tmp = this.get('value');
2399 var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2401 tmp = "http://" + this.get('value');
2403 var text = this.get('value') ? this.node.attrs.text || tmp : '';
2404 this.$el.find('a').attr('href', tmp).text(text);
2407 on_button_clicked: function() {
2408 if (!this.get('value')) {
2409 this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2411 var url = $.trim(this.get('value'));
2412 if(/^www\./i.test(url))
2413 url = 'http://'+url;
2419 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2420 is_field_number: true,
2421 widget_class: 'oe_form_field_float',
2422 init: function (field_manager, node) {
2423 this._super(field_manager, node);
2424 this.internal_set_value(0);
2425 if (this.node.attrs.digits) {
2426 this.digits = this.node.attrs.digits;
2428 this.digits = this.field.digits;
2431 set_value: function(value_) {
2432 if (value_ === false || value_ === undefined) {
2433 // As in GTK client, floats default to 0
2436 this._super.apply(this, [value_]);
2438 focus: function () {
2439 var $input = this.$('input:first');
2440 return $input.length ? $input.select() : false;
2444 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2445 init: function(field_manager, node) {
2446 this._super.apply(this, arguments);
2450 this._super.apply(this, arguments);
2451 this.on("change:effective_readonly", this, function () {
2452 this.display_field();
2453 this.render_value();
2455 this.display_field();
2456 return this._super();
2458 render_value: function() {
2459 this.$('button.select_records').css('visibility', this.get('effective_readonly') ? 'hidden': '');
2461 set_value: function(value_) {
2463 this.set('value', value_ || false);
2464 this.display_field();
2466 display_field: function() {
2468 this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2469 if (this.get('value')) {
2470 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2471 var domain = instance.web.pyeval.eval('domain', this.get('value'));
2472 var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2473 ds.call('search_count', [domain]).then(function (results) {
2474 $('.oe_domain_count', self.$el).text(results + ' records selected');
2475 $('button span', self.$el).text(' Change selection');
2478 $('.oe_domain_count', this.$el).text('0 record selected');
2479 $('button span', this.$el).text(' Select records');
2481 this.$('.select_records').on('click', self.on_click);
2483 on_click: function(ev) {
2484 event.preventDefault();
2486 var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2487 this.pop = new instance.web.form.SelectCreatePopup(this);
2488 this.pop.select_element(
2489 model, {title: 'Select records...'},
2490 [], this.build_context());
2491 this.pop.on("elements_selected", self, function(element_ids) {
2492 if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2493 var search_data = this.pop.searchview.build_search_data();
2494 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2495 domains: search_data.domains,
2496 contexts: search_data.contexts,
2497 group_by_seq: search_data.groupbys || []
2498 }).then(function (results) {
2499 return results.domain;
2503 var domain = [["id", "in", element_ids]];
2504 var domain_done = $.Deferred().resolve(domain);
2506 $.when(domain_done).then(function (domain) {
2507 var domain = self.pop.dataset.domain.concat(domain || []);
2508 self.set_value(domain);
2514 instance.web.DateTimeWidget = instance.web.Widget.extend({
2515 template: "web.datepicker",
2516 jqueryui_object: 'datetimepicker',
2517 type_of_date: "datetime",
2519 'change .oe_datepicker_master': 'change_datetime',
2521 init: function(parent) {
2522 this._super(parent);
2523 this.name = parent.name;
2527 this.$input = this.$el.find('input.oe_datepicker_master');
2528 this.$input_picker = this.$el.find('input.oe_datepicker_container');
2530 $.datepicker.setDefaults({
2531 clearText: _t('Clear'),
2532 clearStatus: _t('Erase the current date'),
2533 closeText: _t('Done'),
2534 closeStatus: _t('Close without change'),
2535 prevText: _t('<Prev'),
2536 prevStatus: _t('Show the previous month'),
2537 nextText: _t('Next>'),
2538 nextStatus: _t('Show the next month'),
2539 currentText: _t('Today'),
2540 currentStatus: _t('Show the current month'),
2541 monthNames: Date.CultureInfo.monthNames,
2542 monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2543 monthStatus: _t('Show a different month'),
2544 yearStatus: _t('Show a different year'),
2545 weekHeader: _t('Wk'),
2546 weekStatus: _t('Week of the year'),
2547 dayNames: Date.CultureInfo.dayNames,
2548 dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2549 dayNamesMin: Date.CultureInfo.shortestDayNames,
2550 dayStatus: _t('Set DD as first week day'),
2551 dateStatus: _t('Select D, M d'),
2552 firstDay: Date.CultureInfo.firstDayOfWeek,
2553 initStatus: _t('Select a date'),
2556 $.timepicker.setDefaults({
2557 timeOnlyTitle: _t('Choose Time'),
2558 timeText: _t('Time'),
2559 hourText: _t('Hour'),
2560 minuteText: _t('Minute'),
2561 secondText: _t('Second'),
2562 currentText: _t('Now'),
2563 closeText: _t('Done')
2567 onClose: this.on_picker_select,
2568 onSelect: this.on_picker_select,
2572 showButtonPanel: true,
2573 firstDay: Date.CultureInfo.firstDayOfWeek
2575 // Some clicks in the datepicker dialog are not stopped by the
2576 // datepicker and "bubble through", unexpectedly triggering the bus's
2577 // click event. Prevent that.
2578 this.picker('widget').click(function (e) { e.stopPropagation(); });
2580 this.$el.find('img.oe_datepicker_trigger').click(function() {
2581 if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2582 self.$input.focus();
2585 self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2586 self.$input_picker.show();
2587 self.picker('show');
2588 self.$input_picker.hide();
2590 this.set_readonly(false);
2591 this.set({'value': false});
2593 picker: function() {
2594 return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2596 on_picker_select: function(text, instance_) {
2597 var date = this.picker('getDate');
2599 .val(date ? this.format_client(date) : '')
2603 set_value: function(value_) {
2604 this.set({'value': value_});
2605 this.$input.val(value_ ? this.format_client(value_) : '');
2607 get_value: function() {
2608 return this.get('value');
2610 set_value_from_ui_: function() {
2611 var value_ = this.$input.val() || false;
2612 this.set({'value': this.parse_client(value_)});
2614 set_readonly: function(readonly) {
2615 this.readonly = readonly;
2616 this.$input.prop('readonly', this.readonly);
2617 this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2619 is_valid_: function() {
2620 var value_ = this.$input.val();
2621 if (value_ === "") {
2625 this.parse_client(value_);
2632 parse_client: function(v) {
2633 return instance.web.parse_value(v, {"widget": this.type_of_date});
2635 format_client: function(v) {
2636 return instance.web.format_value(v, {"widget": this.type_of_date});
2638 change_datetime: function() {
2639 if (this.is_valid_()) {
2640 this.set_value_from_ui_();
2641 this.trigger("datetime_changed");
2644 commit_value: function () {
2645 this.change_datetime();
2649 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2650 jqueryui_object: 'datepicker',
2651 type_of_date: "date"
2654 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2655 template: "FieldDatetime",
2656 build_widget: function() {
2657 return new instance.web.DateTimeWidget(this);
2659 destroy_content: function() {
2660 if (this.datewidget) {
2661 this.datewidget.destroy();
2662 this.datewidget = undefined;
2665 initialize_content: function() {
2666 if (!this.get("effective_readonly")) {
2667 this.datewidget = this.build_widget();
2668 this.datewidget.on('datetime_changed', this, _.bind(function() {
2669 this.internal_set_value(this.datewidget.get_value());
2671 this.datewidget.appendTo(this.$el);
2672 this.setupFocus(this.datewidget.$input);
2675 render_value: function() {
2676 if (!this.get("effective_readonly")) {
2677 this.datewidget.set_value(this.get('value'));
2679 this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2682 is_syntax_valid: function() {
2683 if (!this.get("effective_readonly") && this.datewidget) {
2684 return this.datewidget.is_valid_();
2688 is_false: function() {
2689 return this.get('value') === '' || this._super();
2692 var input = this.datewidget && this.datewidget.$input[0];
2693 return input ? input.focus() : false;
2695 set_dimensions: function (height, width) {
2696 this._super(height, width);
2697 this.datewidget.$input.css('height', height);
2701 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2702 template: "FieldDate",
2703 build_widget: function() {
2704 return new instance.web.DateWidget(this);
2708 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2709 template: 'FieldText',
2711 'keyup': function (e) {
2712 if (e.which === $.ui.keyCode.ENTER) {
2713 e.stopPropagation();
2716 'keypress': function (e) {
2717 if (e.which === $.ui.keyCode.ENTER) {
2718 e.stopPropagation();
2721 'change textarea': 'store_dom_value',
2723 initialize_content: function() {
2725 if (! this.get("effective_readonly")) {
2726 this.$textarea = this.$el.find('textarea');
2727 this.auto_sized = false;
2728 this.default_height = this.$textarea.css('height');
2729 if (this.get("effective_readonly")) {
2730 this.$textarea.attr('disabled', 'disabled');
2732 this.setupFocus(this.$textarea);
2734 this.$textarea = undefined;
2737 commit_value: function () {
2738 if (! this.get("effective_readonly") && this.$textarea) {
2739 this.store_dom_value();
2741 return this._super();
2743 store_dom_value: function () {
2744 this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2746 render_value: function() {
2747 if (! this.get("effective_readonly")) {
2748 var show_value = instance.web.format_value(this.get('value'), this, '');
2749 if (show_value === '') {
2750 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2752 this.$textarea.val(show_value);
2753 if (! this.auto_sized) {
2754 this.auto_sized = true;
2755 this.$textarea.autosize();
2757 this.$textarea.trigger("autosize");
2760 var txt = this.get("value") || '';
2761 this.$(".oe_form_text_content").text(txt);
2764 is_syntax_valid: function() {
2765 if (!this.get("effective_readonly") && this.$textarea) {
2767 instance.web.parse_value(this.$textarea.val(), this, '');
2775 is_false: function() {
2776 return this.get('value') === '' || this._super();
2778 focus: function($el) {
2779 var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2780 return input ? input.focus() : false;
2782 set_dimensions: function (height, width) {
2783 this._super(height, width);
2784 if (!this.get("effective_readonly") && this.$textarea) {
2785 this.$textarea.css({
2794 * FieldTextHtml Widget
2795 * Intended for FieldText widgets meant to display HTML content. This
2796 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2797 * To find more information about CLEditor configutation: go to
2798 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2800 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2801 template: 'FieldTextHtml',
2803 this._super.apply(this, arguments);
2805 initialize_content: function() {
2807 if (! this.get("effective_readonly")) {
2808 self._updating_editor = false;
2809 this.$textarea = this.$el.find('textarea');
2810 var width = ((this.node.attrs || {}).editor_width || '100%');
2811 var height = ((this.node.attrs || {}).editor_height || 250);
2812 this.$textarea.cleditor({
2813 width: width, // width not including margins, borders or padding
2814 height: height, // height not including margins, borders or padding
2815 controls: // controls to add to the toolbar
2816 "bold italic underline strikethrough " +
2817 "| removeformat | bullets numbering | outdent " +
2818 "indent | link unlink | source",
2819 bodyStyle: // style to assign to document body contained within the editor
2820 "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2822 this.$cleditor = this.$textarea.cleditor()[0];
2823 this.$cleditor.change(function() {
2824 if (! self._updating_editor) {
2825 self.$cleditor.updateTextArea();
2826 self.internal_set_value(self.$textarea.val());
2829 if (this.field.translate) {
2830 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"/>')
2831 .click(this.on_translate);
2832 this.$cleditor.$toolbar.append($img);
2836 render_value: function() {
2837 if (! this.get("effective_readonly")) {
2838 this.$textarea.val(this.get('value') || '');
2839 this._updating_editor = true;
2840 this.$cleditor.updateFrame();
2841 this._updating_editor = false;
2843 this.$el.html(this.get('value'));
2848 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2849 template: 'FieldBoolean',
2852 this.$checkbox = $("input", this.$el);
2853 this.setupFocus(this.$checkbox);
2854 this.$el.click(_.bind(function() {
2855 this.internal_set_value(this.$checkbox.is(':checked'));
2857 var check_readonly = function() {
2858 self.$checkbox.prop('disabled', self.get("effective_readonly"));
2859 self.click_disabled_boolean();
2861 this.on("change:effective_readonly", this, check_readonly);
2862 check_readonly.call(this);
2863 this._super.apply(this, arguments);
2865 render_value: function() {
2866 this.$checkbox[0].checked = this.get('value');
2869 var input = this.$checkbox && this.$checkbox[0];
2870 return input ? input.focus() : false;
2872 click_disabled_boolean: function(){
2873 var $disabled = this.$el.find('input[type=checkbox]:disabled');
2874 $disabled.each(function (){
2875 $(this).next('div').remove();
2876 $(this).closest("span").append($('<div class="boolean"></div>'));
2882 The progressbar field expect a float from 0 to 100.
2884 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2885 template: 'FieldProgressBar',
2886 render_value: function() {
2887 this.$el.progressbar({
2888 value: this.get('value') || 0,
2889 disabled: this.get("effective_readonly")
2891 var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2892 this.$('span').html(formatted_value + '%');
2897 The PercentPie field expect a float from 0 to 100.
2899 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
2900 template: 'FieldPercentPie',
2902 render_value: function() {
2903 var value = this.get('value'),
2904 formatted_value = Math.round(value || 0) + '%',
2905 svg = this.$('svg')[0];
2908 nv.addGraph(function() {
2909 var width = 42, height = 42;
2910 var chart = nv.models.pieChart()
2913 .margin({top: 0, right: 0, bottom: 0, left: 0})
2918 .color(['#7C7BAD','#DDD'])
2922 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
2925 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
2929 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
2930 .style({"font-size": "10px", "font-weight": "bold"})
2931 .text(formatted_value);
2940 The FieldBarChart expectsa list of values (indeed)
2942 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
2943 template: 'FieldBarChart',
2945 render_value: function() {
2946 var value = JSON.parse(this.get('value'));
2947 var svg = this.$('svg')[0];
2949 nv.addGraph(function() {
2950 var width = 34, height = 34;
2951 var chart = nv.models.discreteBarChart()
2952 .x(function (d) { return d.tooltip })
2953 .y(function (d) { return d.value })
2956 .margin({top: 0, right: 0, bottom: 0, left: 0})
2959 .transitionDuration(350)
2964 .datum([{key: 'values', values: value}])
2967 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
2969 nv.utils.windowResize(chart.update);
2978 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2979 template: 'FieldSelection',
2981 'change select': 'store_dom_value',
2983 init: function(field_manager, node) {
2985 this._super(field_manager, node);
2986 this.set("value", false);
2987 this.set("values", []);
2988 this.records_orderer = new instance.web.DropMisordered();
2989 this.field_manager.on("view_content_has_changed", this, function() {
2990 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
2991 if (! _.isEqual(domain, this.get("domain"))) {
2992 this.set("domain", domain);
2996 initialize_field: function() {
2997 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
2998 this.on("change:domain", this, this.query_values);
2999 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3000 this.on("change:values", this, this.render_value);
3002 query_values: function() {
3005 if (this.field.type === "many2one") {
3006 var model = new openerp.Model(openerp.session, this.field.relation);
3007 def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3009 var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3010 def = $.when(values);
3012 this.records_orderer.add(def).then(function(values) {
3013 if (! _.isEqual(values, self.get("values"))) {
3014 self.set("values", values);
3018 initialize_content: function() {
3019 // Flag indicating whether we're in an event chain containing a change
3020 // event on the select, in order to know what to do on keyup[RETURN]:
3021 // * If the user presses [RETURN] as part of changing the value of a
3022 // selection, we should just let the value change and not let the
3023 // event broadcast further (e.g. to validating the current state of
3024 // the form in editable list view, which would lead to saving the
3025 // current row or switching to the next one)
3026 // * If the user presses [RETURN] with a select closed (side-effect:
3027 // also if the user opened the select and pressed [RETURN] without
3028 // changing the selected value), takes the action as validating the
3030 var ischanging = false;
3031 var $select = this.$el.find('select')
3032 .change(function () { ischanging = true; })
3033 .click(function () { ischanging = false; })
3034 .keyup(function (e) {
3035 if (e.which !== 13 || !ischanging) { return; }
3036 e.stopPropagation();
3039 this.setupFocus($select);
3041 commit_value: function () {
3042 this.store_dom_value();
3043 return this._super();
3045 store_dom_value: function () {
3046 if (!this.get('effective_readonly') && this.$('select').length) {
3047 var val = JSON.parse(this.$('select').val());
3048 this.internal_set_value(val);
3051 set_value: function(value_) {
3052 value_ = value_ === null ? false : value_;
3053 value_ = value_ instanceof Array ? value_[0] : value_;
3054 this._super(value_);
3056 render_value: function() {
3057 var values = this.get("values");
3058 values = [[false, this.node.attrs.placeholder || '']].concat(values);
3059 var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3061 found = [this.get("value"), _t('Unknown')];
3062 values = [found].concat(values);
3064 if (! this.get("effective_readonly")) {
3065 this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3066 this.$("select").val(JSON.stringify(found[0]));
3068 this.$el.text(found[1]);
3072 var input = this.$('select:first')[0];
3073 return input ? input.focus() : false;
3075 set_dimensions: function (height, width) {
3076 this._super(height, width);
3077 this.$('select').css({
3084 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3085 template: 'FieldRadio',
3087 'click input': 'click_change_value'
3089 init: function(field_manager, node) {
3090 /* Radio button widget: Attributes options:
3091 * - "horizontal" to display in column
3092 * - "no_radiolabel" don't display text values
3094 this._super(field_manager, node);
3095 this.selection = _.clone(this.field.selection) || [];
3096 this.domain = false;
3098 initialize_content: function () {
3099 this.uniqueId = _.uniqueId("radio");
3100 this.on("change:effective_readonly", this, this.render_value);
3101 this.field_manager.on("view_content_has_changed", this, this.get_selection);
3102 this.get_selection();
3104 click_change_value: function (event) {
3105 var val = $(event.target).val();
3106 val = this.field.type == "selection" ? val : +val;
3107 if (val == this.get_value()) {
3108 this.set_value(false);
3110 this.set_value(val);
3113 /** Get the selection and render it
3114 * selection: [[identifier, value_to_display], ...]
3115 * For selection fields: this is directly given by this.field.selection
3116 * For many2one fields: perform a search on the relation of the many2one field
3118 get_selection: function() {
3121 var def = $.Deferred();
3122 if (self.field.type == "many2one") {
3123 var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3124 if (! _.isEqual(self.domain, domain)) {
3125 self.domain = domain;
3126 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3127 ds.call('search', [self.domain])
3128 .then(function (records) {
3129 ds.name_get(records).then(function (records) {
3130 selection = records;
3135 selection = self.selection;
3139 else if (self.field.type == "selection") {
3140 selection = self.field.selection || [];
3143 return def.then(function () {
3144 if (! _.isEqual(selection, self.selection)) {
3145 self.selection = _.clone(selection);
3146 self.renderElement();
3147 self.render_value();
3151 set_value: function (value_) {
3153 if (this.field.type == "selection") {
3154 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3156 else if (!this.selection.length) {
3157 this.selection = [value_];
3160 this._super(value_);
3162 get_value: function () {
3163 var value = this.get('value');
3164 return value instanceof Array ? value[0] : value;
3166 render_value: function () {
3168 this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3169 this.$("input:checked").prop("checked", false);
3170 if (this.get_value()) {
3171 this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3172 this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3177 // jquery autocomplete tweak to allow html and classnames
3179 var proto = $.ui.autocomplete.prototype,
3180 initSource = proto._initSource;
3182 function filter( array, term ) {
3183 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3184 return $.grep( array, function(value_) {
3185 return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3190 _initSource: function() {
3191 if ( this.options.html && $.isArray(this.options.source) ) {
3192 this.source = function( request, response ) {
3193 response( filter( this.options.source, request.term ) );
3196 initSource.call( this );
3200 _renderItem: function( ul, item) {
3201 return $( "<li></li>" )
3202 .data( "item.autocomplete", item )
3203 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3205 .addClass(item.classname);
3211 A mixin containing some useful methods to handle completion inputs.
3213 The widget containing this option can have these arguments in its widget options:
3214 - no_quick_create: if true, it will disable the quick create
3216 instance.web.form.CompletionFieldMixin = {
3219 this.orderer = new instance.web.DropMisordered();
3222 * Call this method to search using a string.
3224 get_search_result: function(search_val) {
3227 var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3228 var blacklist = this.get_search_blacklist();
3229 this.last_query = search_val;
3231 return this.orderer.add(dataset.name_search(
3232 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
3233 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3234 self.last_search = data;
3235 // possible selections for the m2o
3236 var values = _.map(data, function(x) {
3237 x[1] = x[1].split("\n")[0];
3239 label: _.str.escapeHTML(x[1]),
3246 // search more... if more results that max
3247 if (values.length > self.limit) {
3248 values = values.slice(0, self.limit);
3250 label: _t("Search More..."),
3251 action: function() {
3252 dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3253 self._search_create_popup("search", data);
3256 classname: 'oe_m2o_dropdown_option'
3260 var raw_result = _(data.result).map(function(x) {return x[1];});
3261 if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3262 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3264 label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3265 $('<span />').text(search_val).html()),
3266 action: function() {
3267 self._quick_create(search_val);
3269 classname: 'oe_m2o_dropdown_option'
3273 if (!(self.options && self.options.no_create)){
3275 label: _t("Create and Edit..."),
3276 action: function() {
3277 self._search_create_popup("form", undefined, self._create_context(search_val));
3279 classname: 'oe_m2o_dropdown_option'
3286 get_search_blacklist: function() {
3289 _quick_create: function(name) {
3291 var slow_create = function () {
3292 self._search_create_popup("form", undefined, self._create_context(name));
3294 if (self.options.quick_create === undefined || self.options.quick_create) {
3295 new instance.web.DataSet(this, this.field.relation, self.build_context())
3296 .name_create(name).done(function(data) {
3297 if (!self.get('effective_readonly'))
3298 self.add_id(data[0]);
3299 }).fail(function(error, event) {
3300 event.preventDefault();
3306 // all search/create popup handling
3307 _search_create_popup: function(view, ids, context) {
3309 var pop = new instance.web.form.SelectCreatePopup(this);
3311 self.field.relation,
3313 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3314 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3316 disable_multiple_selection: true
3318 self.build_domain(),
3319 new instance.web.CompoundContext(self.build_context(), context || {})
3321 pop.on("elements_selected", self, function(element_ids) {
3322 self.add_id(element_ids[0]);
3329 add_id: function(id) {},
3330 _create_context: function(name) {
3332 var field = (this.options || {}).create_name_field;
3333 if (field === undefined)
3335 if (field !== false && name && (this.options || {}).quick_create !== false)
3336 tmp["default_" + field] = name;
3341 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3342 template: "M2ODialog",
3343 init: function(parent) {
3344 this._super(parent, {
3345 title: _.str.sprintf(_t("Add %s"), parent.string),
3351 this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3352 this.$("input").val(this.getParent().last_query);
3353 this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3354 self.getParent()._quick_create(self.$("input").val());
3357 this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3358 self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3361 this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3367 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3368 template: "FieldMany2One",
3370 'keydown input': function (e) {
3372 case $.ui.keyCode.UP:
3373 case $.ui.keyCode.DOWN:
3374 e.stopPropagation();
3378 init: function(field_manager, node) {
3379 this._super(field_manager, node);
3380 instance.web.form.CompletionFieldMixin.init.call(this);
3381 this.set({'value': false});
3382 this.display_value = {};
3383 this.display_value_backup = {};
3384 this.last_search = [];
3385 this.floating = false;
3386 this.current_display = null;
3387 this.is_started = false;
3389 reinit_value: function(val) {
3390 this.internal_set_value(val);
3391 this.floating = false;
3392 if (this.is_started)
3393 this.render_value();
3395 initialize_field: function() {
3396 this.is_started = true;
3397 instance.web.bus.on('click', this, function() {
3398 if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3399 this.$input.autocomplete("close");
3402 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3404 initialize_content: function() {
3405 if (!this.get("effective_readonly"))
3406 this.render_editable();
3408 destroy_content: function () {
3409 if (this.$drop_down) {
3410 this.$drop_down.off('click');
3411 delete this.$drop_down;
3414 this.$input.closest(".modal .modal-content").off('scroll');
3415 this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3416 'focus focusout change keydown');
3419 if (this.$follow_button) {
3420 this.$follow_button.off('blur focus click');
3421 delete this.$follow_button;
3424 destroy: function () {
3425 this.destroy_content();
3426 return this._super();
3428 init_error_displayer: function() {
3431 hide_error_displayer: function() {
3434 show_error_displayer: function() {
3435 new instance.web.form.M2ODialog(this).open();
3437 render_editable: function() {
3439 this.$input = this.$el.find("input");
3441 this.init_error_displayer();
3443 self.$input.on('focus', function() {
3444 self.hide_error_displayer();
3447 this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3448 this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3450 this.$follow_button.click(function(ev) {
3451 ev.preventDefault();
3452 if (!self.get('value')) {
3456 var pop = new instance.web.form.FormOpenPopup(self);
3457 var context = self.build_context().eval();
3458 var model_obj = new instance.web.Model(self.field.relation);
3459 model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3461 self.field.relation,
3463 self.build_context(),
3465 title: _t("Open: ") + self.string,
3469 pop.on('write_completed', self, function(){
3470 self.display_value = {};
3471 self.display_value_backup = {};
3472 self.render_value();
3474 self.trigger('changed_value');
3479 // some behavior for input
3480 var input_changed = function() {
3481 if (self.current_display !== self.$input.val()) {
3482 self.current_display = self.$input.val();
3483 if (self.$input.val() === "") {
3484 self.internal_set_value(false);
3485 self.floating = false;
3487 self.floating = true;
3491 this.$input.keydown(input_changed);
3492 this.$input.change(input_changed);
3493 this.$drop_down.click(function() {
3494 self.$input.focus();
3495 if (self.$input.autocomplete("widget").is(":visible")) {
3496 self.$input.autocomplete("close");
3498 if (self.get("value") && ! self.floating) {
3499 self.$input.autocomplete("search", "");
3501 self.$input.autocomplete("search");
3506 // Autocomplete close on dialog content scroll
3507 var close_autocomplete = _.debounce(function() {
3508 if (self.$input.autocomplete("widget").is(":visible")) {
3509 self.$input.autocomplete("close");
3512 this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3514 self.ed_def = $.Deferred();
3515 self.uned_def = $.Deferred();
3517 var ed_duration = 15000;
3518 var anyoneLoosesFocus = function (e) {
3520 if (self.floating) {
3521 if (self.last_search.length > 0) {
3522 if (self.last_search[0][0] != self.get("value")) {
3523 self.display_value = {};
3524 self.display_value_backup = {};
3525 self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3526 self.reinit_value(self.last_search[0][0]);
3529 self.render_value();
3533 self.reinit_value(false);
3535 self.floating = false;
3537 if (used && self.get("value") === false && ! self.no_ed && (self.options.no_create === false || self.options.no_create === undefined)) {
3538 self.ed_def.reject();
3539 self.uned_def.reject();
3540 self.ed_def = $.Deferred();
3541 self.ed_def.done(function() {
3542 self.show_error_displayer();
3543 ignore_blur = false;
3544 self.trigger('focused');
3547 setTimeout(function() {
3548 self.ed_def.resolve();
3549 self.uned_def.reject();
3550 self.uned_def = $.Deferred();
3551 self.uned_def.done(function() {
3552 self.hide_error_displayer();
3554 setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3558 self.ed_def.reject();
3561 var ignore_blur = false;
3563 focusout: anyoneLoosesFocus,
3564 focus: function () { self.trigger('focused'); },
3565 autocompleteopen: function () { ignore_blur = true; },
3566 autocompleteclose: function () { ignore_blur = false; },
3568 // autocomplete open
3569 if (ignore_blur) { return; }
3570 if (_(self.getChildren()).any(function (child) {
3571 return child instanceof instance.web.form.AbstractFormPopup;
3573 self.trigger('blurred');
3577 var isSelecting = false;
3579 this.$input.autocomplete({
3580 source: function(req, resp) {
3581 self.get_search_result(req.term).done(function(result) {
3585 select: function(event, ui) {
3589 self.display_value = {};
3590 self.display_value_backup = {};
3591 self.display_value["" + item.id] = item.name;
3592 self.reinit_value(item.id);
3593 } else if (item.action) {
3595 // Cancel widget blurring, to avoid form blur event
3596 self.trigger('focused');
3600 focus: function(e, ui) {
3604 // disabled to solve a bug, but may cause others
3605 //close: anyoneLoosesFocus,
3609 this.$input.autocomplete("widget").openerpClass();
3610 // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3611 this.$input.keyup(function(e) {
3612 if (e.which === 13) { // ENTER
3614 e.stopPropagation();
3616 isSelecting = false;
3618 this.setupFocus(this.$follow_button);
3620 render_value: function(no_recurse) {
3622 if (! this.get("value")) {
3623 this.display_string("");
3626 var display = this.display_value["" + this.get("value")];
3628 this.display_string(display);
3632 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3633 this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3635 self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3638 self.display_value["" + self.get("value")] = data[0][1];
3639 self.render_value(true);
3640 }).fail( function (data, event) {
3641 // avoid displaying crash errors as many2One should be name_get compliant
3642 event.preventDefault();
3643 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3644 self.render_value(true);
3648 display_string: function(str) {
3650 if (!this.get("effective_readonly")) {
3651 this.$input.val(str.split("\n")[0]);
3652 this.current_display = this.$input.val();
3653 if (this.is_false()) {
3654 this.$('.oe_m2o_cm_button').css({'display':'none'});
3656 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3659 var lines = _.escape(str).split("\n");
3663 follow = _.rest(lines).join("<br />");
3666 var $link = this.$el.find('.oe_form_uri')
3669 if (! this.options.no_open)
3670 $link.click(function () {
3671 var context = self.build_context().eval();
3672 var model_obj = new instance.web.Model(self.field.relation);
3673 model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3674 self.do_action(action);
3678 $(".oe_form_m2o_follow", this.$el).html(follow);
3681 set_value: function(value_) {
3683 if (value_ instanceof Array) {
3684 this.display_value = {};
3685 this.display_value_backup = {};
3686 if (! this.options.always_reload) {
3687 this.display_value["" + value_[0]] = value_[1];
3690 this.display_value_backup["" + value_[0]] = value_[1];
3694 value_ = value_ || false;
3695 this.reinit_value(value_);
3697 get_displayed: function() {
3698 return this.display_value["" + this.get("value")];
3700 add_id: function(id) {
3701 this.display_value = {};
3702 this.display_value_backup = {};
3703 this.reinit_value(id);
3705 is_false: function() {
3706 return ! this.get("value");
3708 focus: function () {
3709 var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3710 return input ? input.focus() : false;
3712 _quick_create: function() {
3714 this.ed_def.reject();
3715 return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3717 _search_create_popup: function() {
3719 this.ed_def.reject();
3720 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3722 set_dimensions: function (height, width) {
3723 this._super(height, width);
3724 this.$input.css('height', height);
3728 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3729 template: 'Many2OneButton',
3730 init: function(field_manager, node) {
3731 this._super.apply(this, arguments);
3734 this._super.apply(this, arguments);
3737 set_button: function() {
3740 this.$button.remove();
3743 this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3744 this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3745 this.$button.addClass('oe_link').css({'padding':'4px'});
3746 this.$el.append(this.$button);
3747 this.$button.on('click', self.on_click);
3749 on_click: function(ev) {
3751 this.popup = new instance.web.form.FormOpenPopup(this);
3752 this.popup.show_element(
3753 this.field.relation,
3755 this.build_context(),
3756 {title: this.string}
3758 this.popup.on('create_completed', self, function(r) {
3762 set_value: function(value_) {
3764 if (value_ instanceof Array) {
3767 value_ = value_ || false;
3768 this.set('value', value_);
3774 * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3775 * the big ugly button in the header.
3777 * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3778 * the corresponding field's readonly or effective_readonly property) to
3779 * decide whether the special row should or should not be inserted.
3781 * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3782 * set on the insertion row.
3784 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3785 pad_table_to: function (count) {
3786 if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3791 this._super(count > 0 ? count - 1 : 0);
3794 var columns = _(this.columns).filter(function (column) {
3795 return column.invisible !== '1';
3797 if (this.options.selectable) { columns++; }
3798 if (this.options.deletable) { columns++; }
3800 var $cell = $('<td>', {
3802 'class': this._add_row_class || ''
3804 $('<a>', {href: '#'}).text(_t("Add an item"))
3805 .mousedown(function () {
3806 // FIXME: needs to be an official API somehow
3807 if (self.view.editor.is_editing()) {
3808 self.view.__ignore_blur = true;
3811 .click(function (e) {
3813 e.stopPropagation();
3814 // FIXME: there should also be an API for that one
3815 if (self.view.editor.form.__blur_timeout) {
3816 clearTimeout(self.view.editor.form.__blur_timeout);
3817 self.view.editor.form.__blur_timeout = false;
3819 self.view.ensure_saved().done(function () {
3820 self.view.do_add_record();
3824 var $padding = this.$current.find('tr:not([data-id]):first');
3825 var $newrow = $('<tr>').append($cell);
3826 if ($padding.length) {
3827 $padding.before($newrow);
3829 this.$current.append($newrow)
3835 # Values: (0, 0, { fields }) create
3836 # (1, ID, { fields }) update
3837 # (2, ID) remove (delete)
3838 # (3, ID) unlink one (target id or target of relation)
3840 # (5) unlink all (only valid for one2many)
3845 'create': function (values) {
3846 return [commands.CREATE, false, values];
3848 // (1, id, {values})
3850 'update': function (id, values) {
3851 return [commands.UPDATE, id, values];
3855 'delete': function (id) {
3856 return [commands.DELETE, id, false];
3858 // (3, id[, _]) removes relation, but not linked record itself
3860 'forget': function (id) {
3861 return [commands.FORGET, id, false];
3865 'link_to': function (id) {
3866 return [commands.LINK_TO, id, false];
3870 'delete_all': function () {
3871 return [5, false, false];
3873 // (6, _, ids) replaces all linked records with provided ids
3875 'replace_with': function (ids) {
3876 return [6, false, ids];
3879 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3880 multi_selection: false,
3881 disable_utility_classes: true,
3882 init: function(field_manager, node) {
3883 this._super(field_manager, node);
3884 lazy_build_o2m_kanban_view();
3885 this.is_loaded = $.Deferred();
3886 this.initial_is_loaded = this.is_loaded;
3887 this.form_last_update = $.Deferred();
3888 this.init_form_last_update = this.form_last_update;
3889 this.is_started = false;
3890 this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3891 this.dataset.o2m = this;
3892 this.dataset.parent_view = this.view;
3893 this.dataset.child_name = this.name;
3895 this.dataset.on('dataset_changed', this, function() {
3896 self.trigger_on_change();
3901 this._super.apply(this, arguments);
3902 this.$el.addClass('oe_form_field oe_form_field_one2many');
3907 this.is_loaded.done(function() {
3908 self.on("change:effective_readonly", self, function() {
3909 self.is_loaded = self.is_loaded.then(function() {
3910 self.viewmanager.destroy();
3911 return $.when(self.load_views()).done(function() {
3912 self.reload_current_view();
3917 this.is_started = true;
3918 this.reload_current_view();
3920 trigger_on_change: function() {
3921 this.trigger('changed_value');
3923 load_views: function() {
3926 var modes = this.node.attrs.mode;
3927 modes = !!modes ? modes.split(",") : ["tree"];
3929 _.each(modes, function(mode) {
3930 if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3931 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3935 view_type: mode == "tree" ? "list" : mode,
3938 if (self.field.views && self.field.views[mode]) {
3939 view.embedded_view = self.field.views[mode];
3941 if(view.view_type === "list") {
3942 _.extend(view.options, {
3944 selectable: self.multi_selection,
3946 import_enabled: false,
3949 if (self.get("effective_readonly")) {
3950 _.extend(view.options, {
3955 } else if (view.view_type === "form") {
3956 if (self.get("effective_readonly")) {
3957 view.view_type = 'form';
3959 _.extend(view.options, {
3960 not_interactible_on_create: true,
3962 } else if (view.view_type === "kanban") {
3963 _.extend(view.options, {
3964 confirm_on_delete: false,
3966 if (self.get("effective_readonly")) {
3967 _.extend(view.options, {
3968 action_buttons: false,
3969 quick_creatable: false,
3971 read_only_mode: true,
3979 this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3980 this.viewmanager.o2m = self;
3981 var once = $.Deferred().done(function() {
3982 self.init_form_last_update.resolve();
3984 var def = $.Deferred().done(function() {
3985 self.initial_is_loaded.resolve();
3987 this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3988 controller.o2m = self;
3989 if (view_type == "list") {
3990 if (self.get("effective_readonly")) {
3991 controller.on('edit:before', self, function (e) {
3994 _(controller.columns).find(function (column) {
3995 if (!(column instanceof instance.web.list.Handle)) {
3998 column.modifiers.invisible = true;
4002 } else if (view_type === "form") {
4003 if (self.get("effective_readonly")) {
4004 $(".oe_form_buttons", controller.$el).children().remove();
4006 controller.on("load_record", self, function(){
4009 controller.on('pager_action_executed',self,self.save_any_view);
4010 } else if (view_type == "graph") {
4011 self.reload_current_view();
4015 this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4016 $.when(self.save_any_view()).done(function() {
4017 if (n_mode === "list") {
4018 $.async_when().done(function() {
4019 self.reload_current_view();
4024 $.async_when().done(function () {
4025 self.viewmanager.appendTo(self.$el);
4029 reload_current_view: function() {
4031 self.is_loaded = self.is_loaded.then(function() {
4032 var active_view = self.viewmanager.active_view;
4033 var view = self.viewmanager.views[active_view].controller;
4034 if(active_view === "list") {
4035 return view.reload_content();
4036 } else if (active_view === "form") {
4037 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4038 self.dataset.index = 0;
4040 var act = function() {
4041 return view.do_show();
4043 self.form_last_update = self.form_last_update.then(act, act);
4044 return self.form_last_update;
4045 } else if (view.do_search) {
4046 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
4049 return self.is_loaded;
4051 set_value: function(value_) {
4052 value_ = value_ || [];
4054 this.dataset.reset_ids([]);
4056 if(value_.length >= 1 && value_[0] instanceof Array) {
4058 _.each(value_, function(command) {
4059 var obj = {values: command[2]};
4060 switch (command[0]) {
4061 case commands.CREATE:
4062 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4064 self.dataset.to_create.push(obj);
4065 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4068 case commands.UPDATE:
4069 obj['id'] = command[1];
4070 self.dataset.to_write.push(obj);
4071 self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4074 case commands.DELETE:
4075 self.dataset.to_delete.push({id: command[1]});
4077 case commands.LINK_TO:
4078 ids.push(command[1]);
4080 case commands.DELETE_ALL:
4081 self.dataset.delete_all = true;
4086 this.dataset.set_ids(ids);
4087 } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4089 this.dataset.delete_all = true;
4090 _.each(value_, function(command) {
4091 var obj = {values: command};
4092 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4094 self.dataset.to_create.push(obj);
4095 self.dataset.cache.push(_.clone(obj));
4099 this.dataset.set_ids(ids);
4101 this._super(value_);
4102 this.dataset.reset_ids(value_);
4104 if (this.dataset.index === null && this.dataset.ids.length > 0) {
4105 this.dataset.index = 0;
4107 this.trigger_on_change();
4108 if (this.is_started) {
4109 return self.reload_current_view();
4114 get_value: function() {
4118 var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4119 val = val.concat(_.map(this.dataset.ids, function(id) {
4120 var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4122 return commands.create(alter_order.values);
4124 alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4126 return commands.update(alter_order.id, alter_order.values);
4128 return commands.link_to(id);
4130 return val.concat(_.map(
4131 this.dataset.to_delete, function(x) {
4132 return commands['delete'](x.id);}));
4134 commit_value: function() {
4135 return this.save_any_view();
4137 save_any_view: function() {
4138 if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
4139 this.viewmanager.views[this.viewmanager.active_view] &&
4140 this.viewmanager.views[this.viewmanager.active_view].controller) {
4141 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
4142 if (this.viewmanager.active_view === "form") {
4143 if (view.is_initialized.state() !== 'resolved') {
4144 return $.when(false);
4146 return $.when(view.save());
4147 } else if (this.viewmanager.active_view === "list") {
4148 return $.when(view.ensure_saved());
4151 return $.when(false);
4153 is_syntax_valid: function() {
4154 if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
4156 var view = this.viewmanager.views[this.viewmanager.active_view].controller;
4157 switch (this.viewmanager.active_view) {
4159 return _(view.fields).chain()
4164 return view.is_valid();
4170 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4171 template: 'One2Many.viewmanager',
4172 init: function(parent, dataset, views, flags) {
4173 this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4174 this.registry = this.registry.extend({
4175 list: 'instance.web.form.One2ManyListView',
4176 form: 'instance.web.form.One2ManyFormView',
4177 kanban: 'instance.web.form.One2ManyKanbanView',
4179 this.__ignore_blur = false;
4181 switch_mode: function(mode, unused) {
4182 if (mode !== 'form') {
4183 return this._super(mode, unused);
4186 var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4187 var pop = new instance.web.form.FormOpenPopup(this);
4188 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4189 title: _t("Open: ") + self.o2m.string,
4190 create_function: function(data, options) {
4191 return self.o2m.dataset.create(data, options).done(function(r) {
4192 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4193 self.o2m.dataset.trigger("dataset_changed", r);
4196 write_function: function(id, data, options) {
4197 return self.o2m.dataset.write(id, data, {}).done(function() {
4198 self.o2m.reload_current_view();
4201 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4202 parent_view: self.o2m.view,
4203 child_name: self.o2m.name,
4204 read_function: function() {
4205 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4207 form_view_options: {'not_interactible_on_create':true},
4208 readonly: self.o2m.get("effective_readonly")
4210 pop.on("elements_selected", self, function() {
4211 self.o2m.reload_current_view();
4216 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4217 get_context: function() {
4218 this.context = this.o2m.build_context();
4219 return this.context;
4223 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4224 _template: 'One2Many.listview',
4225 init: function (parent, dataset, view_id, options) {
4226 this._super(parent, dataset, view_id, _.extend(options || {}, {
4227 GroupsType: instance.web.form.One2ManyGroups,
4228 ListType: instance.web.form.One2ManyList
4230 this.on('edit:after', this, this.proxy('_after_edit'));
4231 this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4234 .bind('add', this.proxy("changed_records"))
4235 .bind('edit', this.proxy("changed_records"))
4236 .bind('remove', this.proxy("changed_records"));
4238 start: function () {
4239 var ret = this._super();
4241 .off('mousedown.handleButtons')
4242 .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4245 changed_records: function () {
4246 this.o2m.trigger_on_change();
4248 is_valid: function () {
4249 var editor = this.editor;
4250 var form = editor.form;
4251 // If no edition is pending, the listview can not be invalid (?)
4252 if (!editor.record) {
4255 // If the form has not been modified, the view can only be valid
4256 // NB: is_dirty will also be set on defaults/onchanges/whatever?
4257 // oe_form_dirty seems to only be set on actual user actions
4258 if (!form.$el.is('.oe_form_dirty')) {
4261 this.o2m._dirty_flag = true;
4263 // Otherwise validate internal form
4264 return _(form.fields).chain()
4265 .invoke(function () {
4266 this._check_css_flags();
4267 return this.is_valid();
4272 do_add_record: function () {
4273 if (this.editable()) {
4274 this._super.apply(this, arguments);
4277 var pop = new instance.web.form.SelectCreatePopup(this);
4279 self.o2m.field.relation,
4281 title: _t("Create: ") + self.o2m.string,
4282 initial_view: "form",
4283 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4284 create_function: function(data, options) {
4285 return self.o2m.dataset.create(data, options).done(function(r) {
4286 self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4287 self.o2m.dataset.trigger("dataset_changed", r);
4290 read_function: function() {
4291 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4293 parent_view: self.o2m.view,
4294 child_name: self.o2m.name,
4295 form_view_options: {'not_interactible_on_create':true}
4297 self.o2m.build_domain(),
4298 self.o2m.build_context()
4300 pop.on("elements_selected", self, function() {
4301 self.o2m.reload_current_view();
4305 do_activate_record: function(index, id) {
4307 var pop = new instance.web.form.FormOpenPopup(self);
4308 pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4309 title: _t("Open: ") + self.o2m.string,
4310 write_function: function(id, data) {
4311 return self.o2m.dataset.write(id, data, {}).done(function() {
4312 self.o2m.reload_current_view();
4315 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4316 parent_view: self.o2m.view,
4317 child_name: self.o2m.name,
4318 read_function: function() {
4319 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4321 form_view_options: {'not_interactible_on_create':true},
4322 readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4325 do_button_action: function (name, id, callback) {
4326 if (!_.isNumber(id)) {
4327 instance.webclient.notification.warn(
4328 _t("Action Button"),
4329 _t("The o2m record must be saved before an action can be used"));
4332 var parent_form = this.o2m.view;
4334 this.ensure_saved().then(function () {
4336 return parent_form.save();
4339 }).done(function () {
4340 var ds = self.o2m.dataset;
4341 var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4342 return value.length;
4344 if (!self.o2m.options.reload_on_button && !cached_records) {
4345 self.handle_button(name, id, callback);
4347 self.handle_button(name, id, function(){
4348 self.o2m.view.reload();
4354 _after_edit: function () {
4355 this.__ignore_blur = false;
4356 this.editor.form.on('blurred', this, this._on_form_blur);
4358 // The form's blur thing may be jiggered during the edition setup,
4359 // potentially leading to the o2m instasaving the row. Cancel any
4360 // blurring triggered the edition startup here
4361 this.editor.form.widgetFocused();
4363 _before_unedit: function () {
4364 this.editor.form.off('blurred', this, this._on_form_blur);
4366 _button_down: function () {
4367 // If a button is clicked (usually some sort of action button), it's
4368 // the button's responsibility to ensure the editable list is in the
4369 // correct state -> ignore form blurring
4370 this.__ignore_blur = true;
4373 * Handles blurring of the nested form (saves the currently edited row),
4374 * unless the flag to ignore the event is set to ``true``
4376 * Makes the internal form go away
4378 _on_form_blur: function () {
4379 if (this.__ignore_blur) {
4380 this.__ignore_blur = false;
4383 // FIXME: why isn't there an API for this?
4384 if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4385 this.ensure_saved();
4388 this.cancel_edition();
4390 keypress_ENTER: function () {
4391 // blurring caused by hitting the [Return] key, should skip the
4392 // autosave-on-blur and let the handler for [Return] do its thing (save
4393 // the current row *anyway*, then create a new one/edit the next one)
4394 this.__ignore_blur = true;
4395 this._super.apply(this, arguments);
4397 do_delete: function (ids) {
4398 var confirm = window.confirm;
4399 window.confirm = function () { return true; };
4401 return this._super(ids);
4403 window.confirm = confirm;
4406 reload_record: function (record) {
4407 // Evict record.id from cache to ensure it will be reloaded correctly
4408 this.dataset.evict_record(record.get('id'));
4410 return this._super(record);
4413 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4414 setup_resequence_rows: function () {
4415 if (!this.view.o2m.get('effective_readonly')) {
4416 this._super.apply(this, arguments);
4420 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4421 _add_row_class: 'oe_form_field_one2many_list_row_add',
4422 is_readonly: function () {
4423 return this.view.o2m.get('effective_readonly');
4427 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4428 form_template: 'One2Many.formview',
4429 load_form: function(data) {
4432 this.$buttons.find('button.oe_form_button_create').click(function() {
4433 self.save().done(self.on_button_new);
4436 do_notify_change: function() {
4437 if (this.dataset.parent_view) {
4438 this.dataset.parent_view.do_notify_change();
4440 this._super.apply(this, arguments);
4445 var lazy_build_o2m_kanban_view = function() {
4446 if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4448 instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4452 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4453 template: "FieldMany2ManyTags",
4454 tag_template: "FieldMany2ManyTag",
4456 this._super.apply(this, arguments);
4457 instance.web.form.CompletionFieldMixin.init.call(this);
4458 this.set({"value": []});
4459 this._display_orderer = new instance.web.DropMisordered();
4460 this._drop_shown = false;
4462 initialize_texttext: function(){
4465 plugins : 'tags arrow autocomplete',
4467 render: function(suggestion) {
4468 return $('<span class="text-label"/>').
4469 data('index', suggestion['index']).html(suggestion['label']);
4474 selectFromDropdown: function() {
4475 this.trigger('hideDropdown');
4476 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4477 var data = self.search_result[index];
4479 self.add_id(data.id);
4481 self.ignore_blur = true;
4484 this.trigger('setSuggestions', {result : []});
4488 isTagAllowed: function(tag) {
4492 removeTag: function(tag) {
4493 var id = tag.data("id");
4494 self.set({"value": _.without(self.get("value"), id)});
4496 renderTag: function(stuff) {
4497 return $.fn.textext.TextExtTags.prototype.renderTag.
4498 call(this, stuff).data("id", stuff.id);
4502 itemToString: function(item) {
4507 onSetInputData: function(e, data) {
4509 this._plugins.autocomplete._suggestions = null;
4511 this.input().val(data);
4517 initialize_content: function() {
4518 if (this.get("effective_readonly"))
4521 self.ignore_blur = false;
4522 self.$text = this.$("textarea");
4523 self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4525 var str = !!data ? data.query || '' : '';
4526 self.get_search_result(str).done(function(result) {
4527 self.search_result = result;
4528 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4529 return _.extend(el, {index:i});
4532 }).bind('hideDropdown', function() {
4533 self._drop_shown = false;
4534 }).bind('showDropdown', function() {
4535 self._drop_shown = true;
4537 self.tags = self.$text.textext()[0].tags();
4539 .focusin(function () {
4540 self.trigger('focused');
4541 self.ignore_blur = false;
4543 .focusout(function() {
4544 self.$text.trigger("setInputData", "");
4545 if (!self.ignore_blur) {
4546 self.trigger('blurred');
4548 }).keydown(function(e) {
4549 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4550 self.$text.textext()[0].autocomplete().selectFromDropdown();
4554 set_value: function(value_) {
4555 value_ = value_ || [];
4556 if (value_.length >= 1 && value_[0] instanceof Array) {
4557 value_ = value_[0][2];
4559 this._super(value_);
4561 is_false: function() {
4562 return _(this.get("value")).isEmpty();
4564 get_value: function() {
4565 var tmp = [commands.replace_with(this.get("value"))];
4568 get_search_blacklist: function() {
4569 return this.get("value");
4571 map_tag: function(data){
4572 return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4574 get_render_data: function(ids){
4576 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4577 return dataset.name_get(ids);
4579 render_tag: function(data) {
4581 if (! self.get("effective_readonly")) {
4582 self.tags.containerElement().children().remove();
4583 self.$('textarea').css("padding-left", "3px");
4584 self.tags.addTags(self.map_tag(data));
4586 self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4589 render_value: function() {
4591 var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4592 var values = self.get("value");
4593 var handle_names = function(data) {
4594 if (self.isDestroyed())
4597 _.each(data, function(el) {
4598 indexed[el[0]] = el;
4600 data = _.map(values, function(el) { return indexed[el]; });
4601 self.render_tag(data);
4603 if (! values || values.length > 0) {
4604 this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4610 add_id: function(id) {
4611 this.set({'value': _.uniq(this.get('value').concat([id]))});
4613 focus: function () {
4614 var input = this.$text && this.$text[0];
4615 return input ? input.focus() : false;
4617 set_dimensions: function (height, width) {
4618 this._super(height, width);
4619 this.$("textarea").css({
4624 _search_create_popup: function() {
4625 self.ignore_blur = true;
4626 return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4632 - reload_on_button: Reload the whole form view if click on a button in a list view.
4633 If you see this options, do not use it, it's basically a dirty hack to make one
4634 precise o2m to behave the way we want.
4636 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4637 multi_selection: false,
4638 disable_utility_classes: true,
4639 init: function(field_manager, node) {
4640 this._super(field_manager, node);
4641 this.is_loaded = $.Deferred();
4642 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4643 this.dataset.m2m = this;
4645 this.dataset.on('unlink', self, function(ids) {
4646 self.dataset_changed();
4649 this.list_dm = new instance.web.DropMisordered();
4650 this.render_value_dm = new instance.web.DropMisordered();
4652 initialize_content: function() {
4655 this.$el.addClass('oe_form_field oe_form_field_many2many');
4657 this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4659 'deletable': this.get("effective_readonly") ? false : true,
4660 'selectable': this.multi_selection,
4662 'reorderable': false,
4663 'import_enabled': false,
4665 var embedded = (this.field.views || {}).tree;
4667 this.list_view.set_embedded_view(embedded);
4669 this.list_view.m2m_field = this;
4670 var loaded = $.Deferred();
4671 this.list_view.on("list_view_loaded", this, function() {
4674 this.list_view.appendTo(this.$el);
4676 var old_def = self.is_loaded;
4677 self.is_loaded = $.Deferred().done(function() {
4680 this.list_dm.add(loaded).then(function() {
4681 self.is_loaded.resolve();
4684 destroy_content: function() {
4685 this.list_view.destroy();
4686 this.list_view = undefined;
4688 set_value: function(value_) {
4689 value_ = value_ || [];
4690 if (value_.length >= 1 && value_[0] instanceof Array) {
4691 value_ = value_[0][2];
4693 this._super(value_);
4695 get_value: function() {
4696 return [commands.replace_with(this.get('value'))];
4698 is_false: function () {
4699 return _(this.get("value")).isEmpty();
4701 render_value: function() {
4703 this.dataset.set_ids(this.get("value"));
4704 this.render_value_dm.add(this.is_loaded).then(function() {
4705 return self.list_view.reload_content();
4708 dataset_changed: function() {
4709 this.internal_set_value(this.dataset.ids);
4713 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4714 get_context: function() {
4715 this.context = this.m2m.build_context();
4716 return this.context;
4722 * @extends instance.web.ListView
4724 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4725 init: function (parent, dataset, view_id, options) {
4726 this._super(parent, dataset, view_id, _.extend(options || {}, {
4727 ListType: instance.web.form.Many2ManyList,
4730 do_add_record: function () {
4731 var pop = new instance.web.form.SelectCreatePopup(this);
4735 title: _t("Add: ") + this.m2m_field.string
4737 new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4738 this.m2m_field.build_context()
4741 pop.on("elements_selected", self, function(element_ids) {
4743 _(element_ids).each(function (id) {
4744 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4745 self.dataset.set_ids(self.dataset.ids.concat([id]));
4746 self.m2m_field.dataset_changed();
4751 self.reload_content();
4755 do_activate_record: function(index, id) {
4757 var pop = new instance.web.form.FormOpenPopup(this);
4758 pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4759 title: _t("Open: ") + this.m2m_field.string,
4760 readonly: this.getParent().get("effective_readonly")
4762 pop.on('write_completed', self, self.reload_content);
4764 do_button_action: function(name, id, callback) {
4766 var _sup = _.bind(this._super, this);
4767 if (! this.m2m_field.options.reload_on_button) {
4768 return _sup(name, id, callback);
4770 return this.m2m_field.view.save().then(function() {
4771 return _sup(name, id, function() {
4772 self.m2m_field.view.reload();
4777 is_action_enabled: function () { return true; },
4779 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4780 _add_row_class: 'oe_form_field_many2many_list_row_add',
4781 is_readonly: function () {
4782 return this.view.m2m_field.get('effective_readonly');
4786 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4787 disable_utility_classes: true,
4788 init: function(field_manager, node) {
4789 this._super(field_manager, node);
4790 instance.web.form.CompletionFieldMixin.init.call(this);
4791 m2m_kanban_lazy_init();
4792 this.is_loaded = $.Deferred();
4793 this.initial_is_loaded = this.is_loaded;
4796 this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4797 this.dataset.m2m = this;
4798 this.dataset.on('unlink', self, function(ids) {
4799 self.dataset_changed();
4803 this._super.apply(this, arguments);
4808 self.on("change:effective_readonly", self, function() {
4809 self.is_loaded = self.is_loaded.then(function() {
4810 self.kanban_view.destroy();
4811 return $.when(self.load_view()).done(function() {
4812 self.render_value();
4817 set_value: function(value_) {
4818 value_ = value_ || [];
4819 if (value_.length >= 1 && value_[0] instanceof Array) {
4820 value_ = value_[0][2];
4822 this._super(value_);
4824 get_value: function() {
4825 return [commands.replace_with(this.get('value'))];
4827 load_view: function() {
4829 this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4830 'create_text': _t("Add"),
4831 'creatable': self.get("effective_readonly") ? false : true,
4832 'quick_creatable': self.get("effective_readonly") ? false : true,
4833 'read_only_mode': self.get("effective_readonly") ? true : false,
4834 'confirm_on_delete': false,
4836 var embedded = (this.field.views || {}).kanban;
4838 this.kanban_view.set_embedded_view(embedded);
4840 this.kanban_view.m2m = this;
4841 var loaded = $.Deferred();
4842 this.kanban_view.on("kanban_view_loaded",self,function() {
4843 self.initial_is_loaded.resolve();
4846 this.kanban_view.on('switch_mode', this, this.open_popup);
4847 $.async_when().done(function () {
4848 self.kanban_view.appendTo(self.$el);
4852 render_value: function() {
4854 this.dataset.set_ids(this.get("value"));
4855 this.is_loaded = this.is_loaded.then(function() {
4856 return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4859 dataset_changed: function() {
4860 this.set({'value': this.dataset.ids});
4862 open_popup: function(type, unused) {
4863 if (type !== "form")
4867 if (this.dataset.index === null) {
4868 pop = new instance.web.form.SelectCreatePopup(this);
4870 this.field.relation,
4872 title: _t("Add: ") + this.string
4874 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4875 this.build_context()
4877 pop.on("elements_selected", self, function(element_ids) {
4878 _.each(element_ids, function(one_id) {
4879 if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4880 self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4881 self.dataset_changed();
4882 self.render_value();
4887 var id = self.dataset.ids[self.dataset.index];
4888 pop = new instance.web.form.FormOpenPopup(this);
4889 pop.show_element(self.field.relation, id, self.build_context(), {
4890 title: _t("Open: ") + self.string,
4891 write_function: function(id, data, options) {
4892 return self.dataset.write(id, data, {}).done(function() {
4893 self.render_value();
4896 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4897 parent_view: self.view,
4898 child_name: self.name,
4899 readonly: self.get("effective_readonly")
4903 add_id: function(id) {
4904 this.quick_create.add_id(id);
4908 function m2m_kanban_lazy_init() {
4909 if (instance.web.form.Many2ManyKanbanView)
4911 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4912 quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4913 _is_quick_create_enabled: function() {
4914 return this._super() && ! this.group_by;
4917 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4918 template: 'Many2ManyKanban.quick_create',
4921 * close_btn: If true, the widget will display a "Close" button able to trigger
4924 init: function(parent, dataset, context, buttons) {
4925 this._super(parent);
4926 this.m2m = this.getParent().view.m2m;
4927 this.m2m.quick_create = this;
4928 this._dataset = dataset;
4929 this._buttons = buttons || false;
4930 this._context = context || {};
4932 start: function () {
4934 self.$text = this.$el.find('input').css("width", "200px");
4935 self.$text.textext({
4936 plugins : 'arrow autocomplete',
4938 render: function(suggestion) {
4939 return $('<span class="text-label"/>').
4940 data('index', suggestion['index']).html(suggestion['label']);
4945 selectFromDropdown: function() {
4946 $(this).trigger('hideDropdown');
4947 var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4948 var data = self.search_result[index];
4950 self.add_id(data.id);
4957 itemToString: function(item) {
4962 }).bind('getSuggestions', function(e, data) {
4964 var str = !!data ? data.query || '' : '';
4965 self.m2m.get_search_result(str).done(function(result) {
4966 self.search_result = result;
4967 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4968 return _.extend(el, {index:i});
4972 self.$text.focusout(function() {
4977 this.$text[0].focus();
4979 add_id: function(id) {
4982 self.trigger('added', id);
4983 this.m2m.dataset_changed();
4989 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4991 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4992 template: "AbstractFormPopup.render",
4995 * -readonly: only applicable when not in creation mode, default to false
4996 * - alternative_form_view
5003 * - form_view_options
5005 init_popup: function(model, row_id, domain, context, options) {
5006 this.row_id = row_id;
5008 this.domain = domain || [];
5009 this.context = context || {};
5010 this.options = options;
5011 _.defaults(this.options, {
5014 init_dataset: function() {
5016 this.created_elements = [];
5017 this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5018 this.dataset.read_function = this.options.read_function;
5019 this.dataset.create_function = function(data, options, sup) {
5020 var fct = self.options.create_function || sup;
5021 return fct.call(this, data, options).done(function(r) {
5022 self.trigger('create_completed saved', r);
5023 self.created_elements.push(r);
5026 this.dataset.write_function = function(id, data, options, sup) {
5027 var fct = self.options.write_function || sup;
5028 return fct.call(this, id, data, options).done(function(r) {
5029 self.trigger('write_completed saved', r);
5032 this.dataset.parent_view = this.options.parent_view;
5033 this.dataset.child_name = this.options.child_name;
5035 display_popup: function() {
5037 this.renderElement();
5038 var dialog = new instance.web.Dialog(this, {
5039 dialogClass: 'oe_act_window',
5040 title: this.options.title || "",
5041 }, this.$el).open();
5042 dialog.on('closing', this, function (e){
5043 self.check_exit(true);
5045 this.$buttonpane = dialog.$buttons;
5048 setup_form_view: function() {
5051 this.dataset.ids = [this.row_id];
5052 this.dataset.index = 0;
5054 this.dataset.index = null;
5056 var options = _.clone(self.options.form_view_options) || {};
5057 if (this.row_id !== null) {
5058 options.initial_mode = this.options.readonly ? "view" : "edit";
5061 $buttons: this.$buttonpane,
5063 this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5064 if (this.options.alternative_form_view) {
5065 this.view_form.set_embedded_view(this.options.alternative_form_view);
5067 this.view_form.appendTo(this.$el.find(".oe_popup_form"));
5068 this.view_form.on("form_view_loaded", self, function() {
5069 var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5070 self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5071 multi_select: multi_select,
5072 readonly: self.row_id !== null && self.options.readonly,
5074 var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5075 $snbutton.click(function() {
5076 $.when(self.view_form.save()).done(function() {
5077 self.view_form.reload_mutex.exec(function() {
5078 self.view_form.on_button_new();
5082 var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5083 $sbutton.click(function() {
5084 $.when(self.view_form.save()).done(function() {
5085 self.view_form.reload_mutex.exec(function() {
5090 var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5091 $cbutton.click(function() {
5092 self.view_form.trigger('on_button_cancel');
5095 self.view_form.do_show();
5098 select_elements: function(element_ids) {
5099 this.trigger("elements_selected", element_ids);
5101 check_exit: function(no_destroy) {
5102 if (this.created_elements.length > 0) {
5103 this.select_elements(this.created_elements);
5104 this.created_elements = [];
5106 this.trigger('closed');
5109 destroy: function () {
5110 this.trigger('closed');
5111 if (this.$el.is(":data(bs.modal)")) {
5112 this.$el.parents('.modal').modal('hide');
5119 * Class to display a popup containing a form view.
5121 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5122 show_element: function(model, row_id, context, options) {
5123 this.init_popup(model, row_id, [], context, options);
5124 _.defaults(this.options, {
5126 this.display_popup();
5130 this.init_dataset();
5131 this.setup_form_view();
5136 * Class to display a popup to display a list to search a row. It also allows
5137 * to switch to a form view to create a new row.
5139 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5143 * - initial_view: form or search (default search)
5144 * - disable_multiple_selection
5145 * - list_view_options
5147 select_element: function(model, options, domain, context) {
5148 this.init_popup(model, null, domain, context, options);
5150 _.defaults(this.options, {
5151 initial_view: "search",
5153 this.initial_ids = this.options.initial_ids;
5154 this.display_popup();
5158 this.init_dataset();
5159 if (this.options.initial_view == "search") {
5160 instance.web.pyeval.eval_domains_and_contexts({
5162 contexts: [this.context]
5163 }).done(function (results) {
5164 var search_defaults = {};
5165 _.each(results.context, function (value_, key) {
5166 var match = /^search_default_(.*)$/.exec(key);
5168 search_defaults[match[1]] = value_;
5171 self.setup_search_view(search_defaults);
5177 setup_search_view: function(search_defaults) {
5179 if (this.searchview) {
5180 this.searchview.destroy();
5182 this.searchview = new instance.web.SearchView(this,
5183 this.dataset, false, search_defaults);
5184 this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5185 if (self.initial_ids) {
5186 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5187 contexts.concat(self.context), groupbys);
5188 self.initial_ids = undefined;
5190 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5193 this.searchview.on("search_view_loaded", self, function() {
5194 self.view_list = new instance.web.form.SelectCreateListView(self,
5195 self.dataset, false,
5196 _.extend({'deletable': false,
5197 'selectable': !self.options.disable_multiple_selection,
5198 'import_enabled': false,
5199 '$buttons': self.$buttonpane,
5200 'disable_editable_mode': true,
5201 '$pager': self.$('.oe_popup_list_pager'),
5202 }, self.options.list_view_options || {}));
5203 self.view_list.on('edit:before', self, function (e) {
5206 self.view_list.popup = self;
5207 self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
5208 self.view_list.do_show();
5209 }).then(function() {
5210 self.searchview.do_search();
5212 self.view_list.on("list_view_loaded", self, function() {
5213 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5214 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5215 $cbutton.click(function() {
5218 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5219 $sbutton.click(function() {
5220 self.select_elements(self.selected_ids);
5223 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5224 $cbutton.click(function() {
5229 this.searchview.appendTo($(".oe_popup_search", self.$el));
5231 do_search: function(domains, contexts, groupbys) {
5233 instance.web.pyeval.eval_domains_and_contexts({
5234 domains: domains || [],
5235 contexts: contexts || [],
5236 group_by_seq: groupbys || []
5237 }).done(function (results) {
5238 self.view_list.do_search(results.domain, results.context, results.group_by);
5241 on_click_element: function(ids) {
5243 this.selected_ids = ids || [];
5244 if(this.selected_ids.length > 0) {
5245 self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5247 self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5250 new_object: function() {
5251 if (this.searchview) {
5252 this.searchview.hide();
5254 if (this.view_list) {
5255 this.view_list.do_hide();
5257 this.setup_form_view();
5261 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5262 do_add_record: function () {
5263 this.popup.new_object();
5265 select_record: function(index) {
5266 this.popup.select_elements([this.dataset.ids[index]]);
5267 this.popup.destroy();
5269 do_select: function(ids, records) {
5270 this._super(ids, records);
5271 this.popup.on_click_element(ids);
5275 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5276 template: 'FieldReference',
5277 init: function(field_manager, node) {
5278 this._super(field_manager, node);
5279 this.reference_ready = true;
5281 destroy_content: function() {
5284 this.fm = undefined;
5287 initialize_content: function() {
5289 var fm = new instance.web.form.DefaultFieldManager(this);
5291 fm.extend_field_desc({
5293 selection: this.field_manager.get_field_desc(this.name).selection,
5301 this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5303 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5305 this.selection.on("change:value", this, this.on_selection_changed);
5306 this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5308 .on('focused', null, function () {self.trigger('focused');})
5309 .on('blurred', null, function () {self.trigger('blurred');});
5311 this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5312 name: 'Referenced Document',
5313 modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5315 this.m2o.on("change:value", this, this.data_changed);
5316 this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5318 .on('focused', null, function () {self.trigger('focused');})
5319 .on('blurred', null, function () {self.trigger('blurred');});
5321 on_selection_changed: function() {
5322 if (this.reference_ready) {
5323 this.internal_set_value([this.selection.get_value(), false]);
5324 this.render_value();
5327 data_changed: function() {
5328 if (this.reference_ready) {
5329 this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5332 set_value: function(val) {
5334 val = val.split(',');
5335 val[0] = val[0] || false;
5336 val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5338 this._super(val || [false, false]);
5340 get_value: function() {
5341 return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5343 render_value: function() {
5344 this.reference_ready = false;
5345 if (!this.get("effective_readonly")) {
5346 this.selection.set_value(this.get('value')[0]);
5348 this.m2o.field.relation = this.get('value')[0];
5349 this.m2o.set_value(this.get('value')[1]);
5350 this.m2o.$el.toggle(!!this.get('value')[0]);
5351 this.reference_ready = true;
5355 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5356 init: function(field_manager, node) {
5358 this._super(field_manager, node);
5359 this.binary_value = false;
5360 this.useFileAPI = !!window.FileReader;
5361 this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5362 if (!this.useFileAPI) {
5363 this.fileupload_id = _.uniqueId('oe_fileupload');
5364 $(window).on(this.fileupload_id, function() {
5365 var args = [].slice.call(arguments).slice(1);
5366 self.on_file_uploaded.apply(self, args);
5371 if (!this.useFileAPI) {
5372 $(window).off(this.fileupload_id);
5374 this._super.apply(this, arguments);
5376 initialize_content: function() {
5377 this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5378 this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5379 this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5381 on_file_change: function(e) {
5383 var file_node = e.target;
5384 if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5385 if (this.useFileAPI) {
5386 var file = file_node.files[0];
5387 if (file.size > this.max_upload_size) {
5388 var msg = _t("The selected file exceed the maximum file size of %s.");
5389 instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5392 var filereader = new FileReader();
5393 filereader.readAsDataURL(file);
5394 filereader.onloadend = function(upload) {
5395 var data = upload.target.result;
5396 data = data.split(',')[1];
5397 self.on_file_uploaded(file.size, file.name, file.type, data);
5400 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5401 this.$el.find('form.oe_form_binary_form').submit();
5403 this.$el.find('.oe_form_binary_progress').show();
5404 this.$el.find('.oe_form_binary').hide();
5407 on_file_uploaded: function(size, name, content_type, file_base64) {
5408 if (size === false) {
5409 this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5410 // TODO: use openerp web crashmanager
5411 console.warn("Error while uploading file : ", name);
5413 this.filename = name;
5414 this.on_file_uploaded_and_valid.apply(this, arguments);
5416 this.$el.find('.oe_form_binary_progress').hide();
5417 this.$el.find('.oe_form_binary').show();
5419 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5421 on_save_as: function(ev) {
5422 var value = this.get('value');
5424 this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5425 ev.stopPropagation();
5427 instance.web.blockUI();
5428 var c = instance.webclient.crashmanager;
5429 this.session.get_file({
5430 url: '/web/binary/saveas_ajax',
5431 data: {data: JSON.stringify({
5432 model: this.view.dataset.model,
5433 id: (this.view.datarecord.id || ''),
5435 filename_field: (this.node.attrs.filename || ''),
5436 data: instance.web.form.is_bin_size(value) ? null : value,
5437 context: this.view.dataset.get_context()
5439 complete: instance.web.unblockUI,
5440 error: c.rpc_error.bind(c)
5442 ev.stopPropagation();
5446 set_filename: function(value) {
5447 var filename = this.node.attrs.filename;
5450 tmp[filename] = value;
5451 this.field_manager.set_values(tmp);
5454 on_clear: function() {
5455 if (this.get('value') !== false) {
5456 this.binary_value = false;
5457 this.internal_set_value(false);
5463 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5464 template: 'FieldBinaryFile',
5465 initialize_content: function() {
5467 if (this.get("effective_readonly")) {
5469 this.$el.find('a').click(function(ev) {
5470 if (self.get('value')) {
5471 self.on_save_as(ev);
5477 render_value: function() {
5479 if (!this.get("effective_readonly")) {
5480 if (this.node.attrs.filename) {
5481 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5483 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5485 this.$el.find('input').eq(0).val(show_value);
5487 this.$el.find('a').toggle(!!this.get('value'));
5488 if (this.get('value')) {
5489 show_value = _t("Download");
5491 show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5492 this.$el.find('a').text(show_value);
5496 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5497 this.binary_value = true;
5498 this.internal_set_value(file_base64);
5499 var show_value = name + " (" + instance.web.human_size(size) + ")";
5500 this.$el.find('input').eq(0).val(show_value);
5501 this.set_filename(name);
5503 on_clear: function() {
5504 this._super.apply(this, arguments);
5505 this.$el.find('input').eq(0).val('');
5506 this.set_filename('');
5510 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5511 template: 'FieldBinaryImage',
5512 placeholder: "/web/static/src/img/placeholder.png",
5513 render_value: function() {
5516 if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5517 url = 'data:image/png;base64,' + this.get('value');
5518 } else if (this.get('value')) {
5519 var id = JSON.stringify(this.view.datarecord.id || null);
5520 var field = this.name;
5521 if (this.options.preview_image)
5522 field = this.options.preview_image;
5523 url = this.session.url('/web/binary/image', {
5524 model: this.view.dataset.model,
5527 t: (new Date().getTime()),
5530 url = this.placeholder;
5532 var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5533 $($img).click(function(e) {
5534 if(self.view.get("actual_mode") == "view") {
5535 var $button = $(".oe_form_button_edit");
5536 $button.openerpBounce();
5537 e.stopPropagation();
5540 this.$el.find('> img').remove();
5541 this.$el.prepend($img);
5542 $img.load(function() {
5543 if (! self.options.size)
5545 $img.css("max-width", "" + self.options.size[0] + "px");
5546 $img.css("max-height", "" + self.options.size[1] + "px");
5547 $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5548 $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5550 $img.on('error', function() {
5551 $img.attr('src', self.placeholder);
5552 instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5555 on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5556 this.internal_set_value(file_base64);
5557 this.binary_value = true;
5558 this.render_value();
5559 this.set_filename(name);
5561 on_clear: function() {
5562 this._super.apply(this, arguments);
5563 this.render_value();
5564 this.set_filename('');
5569 * Widget for (many2many field) to upload one or more file in same time and display in list.
5570 * The user can delete his files.
5571 * Options on attribute ; "blockui" {Boolean} block the UI or not
5572 * during the file is uploading
5574 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5575 template: "FieldBinaryFileUploader",
5576 init: function(field_manager, node) {
5577 this._super(field_manager, node);
5578 this.field_manager = field_manager;
5580 if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5581 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);
5585 this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5586 this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5587 $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5591 this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5593 set_value: function(value_) {
5594 value_ = value_ || [];
5595 if (value_.length >= 1 && value_[0] instanceof Array) {
5596 value_ = value_[0][2];
5598 this._super(value_);
5600 get_value: function() {
5601 var tmp = [commands.replace_with(this.get("value"))];
5604 get_file_url: function (attachment) {
5605 return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5607 read_name_values : function () {
5609 // don't reset know values
5610 var ids = this.get('value');
5611 var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5612 // send request for get_name
5613 if (_value.length) {
5614 return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5615 _.each(datas, function (data) {
5616 data.no_unlink = true;
5617 data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5618 self.data[data.id] = data;
5626 render_value: function () {
5628 this.read_name_values().then(function (ids) {
5629 var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5630 render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5631 self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5633 // reinit input type file
5634 var $input = self.$('input.oe_form_binary_file');
5635 $input.after($input.clone(true)).remove();
5636 self.$(".oe_fileupload").show();
5640 on_file_change: function (event) {
5641 event.stopPropagation();
5643 var $target = $(event.target);
5644 if ($target.val() !== '') {
5645 var filename = $target.val().replace(/.*[\\\/]/,'');
5646 // don't uplode more of one file in same time
5647 if (self.data[0] && self.data[0].upload ) {
5650 for (var id in this.get('value')) {
5651 // if the files exits, delete the file before upload (if it's a new file)
5652 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5653 self.ds_file.unlink([id]);
5658 if(this.node.attrs.blockui>0) {
5659 instance.web.blockUI();
5662 // TODO : unactivate send on wizard and form
5665 this.$('form.oe_form_binary_form').submit();
5666 this.$(".oe_fileupload").hide();
5667 // add file on data result
5671 'filename': filename,
5677 on_file_loaded: function (event, result) {
5678 var files = this.get('value');
5681 if(this.node.attrs.blockui>0) {
5682 instance.web.unblockUI();
5685 if (result.error || !result.id ) {
5686 this.do_warn( _t('Uploading Error'), result.error);
5687 delete this.data[0];
5689 if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5690 delete this.data[0];
5691 this.data[result.id] = {
5693 'name': result.name,
5694 'filename': result.filename,
5695 'url': this.get_file_url(result)
5698 this.data[result.id] = {
5700 'name': result.name,
5701 'filename': result.filename,
5702 'url': this.get_file_url(result)
5705 var values = _.clone(this.get('value'));
5706 values.push(result.id);
5707 this.set({'value': values});
5709 this.render_value();
5711 on_file_delete: function (event) {
5712 event.stopPropagation();
5713 var file_id=$(event.target).data("id");
5715 var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5716 if(!this.data[file_id].no_unlink) {
5717 this.ds_file.unlink([file_id]);
5719 this.set({'value': files});
5724 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5725 template: "FieldStatus",
5726 init: function(field_manager, node) {
5727 this._super(field_manager, node);
5728 this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5729 this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5730 this.set({value: false});
5731 this.selection = {'unfolded': [], 'folded': []};
5732 this.set("selection", {'unfolded': [], 'folded': []});
5733 this.selection_dm = new instance.web.DropMisordered();
5734 this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5737 this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5739 this.on("change:value", this, this.get_selection);
5740 this.on("change:evaluated_selection_domain", this, this.get_selection);
5741 this.on("change:selection", this, function() {
5742 this.selection = this.get("selection");
5743 this.render_value();
5745 this.get_selection();
5746 if (this.options.clickable) {
5747 this.$el.on('click','li[data-id]',this.on_click_stage);
5749 if (this.$el.parent().is('header')) {
5750 this.$el.after('<div class="oe_clear"/>');
5754 set_value: function(value_) {
5755 if (value_ instanceof Array) {
5758 this._super(value_);
5760 render_value: function() {
5762 var content = QWeb.render("FieldStatus.content", {
5764 'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5766 self.$el.html(content);
5768 calc_domain: function() {
5769 var d = instance.web.pyeval.eval('domain', this.build_domain());
5770 var domain = []; //if there is no domain defined, fetch all the records
5773 domain = ['|',['id', '=', this.get('value')]].concat(d);
5776 if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5777 this.set("evaluated_selection_domain", domain);
5780 /** Get the selection and render it
5781 * selection: [[identifier, value_to_display], ...]
5782 * For selection fields: this is directly given by this.field.selection
5783 * For many2one fields: perform a search on the relation of the many2one field
5785 get_selection: function() {
5787 var selection_unfolded = [];
5788 var selection_folded = [];
5789 var fold_field = this.options.fold_field;
5791 var calculation = _.bind(function() {
5792 if (this.field.type == "many2one") {
5793 return self.get_distant_fields().then(function (fields) {
5794 return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5795 .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5796 var ids = _.pluck(records, 'id');
5797 return self.dataset.name_get(ids).then(function (records_name) {
5798 _.each(records, function (record) {
5799 var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5800 if (fold_field && record[fold_field] && record.id != self.get('value')) {
5801 selection_folded.push([record.id, name]);
5803 selection_unfolded.push([record.id, name]);
5810 // For field type selection filter values according to
5811 // statusbar_visible attribute of the field. For example:
5812 // statusbar_visible="draft,open".
5813 var select = this.field.selection;
5814 for(var i=0; i < select.length; i++) {
5815 var key = select[i][0];
5816 if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5817 selection_unfolded.push(select[i]);
5823 this.selection_dm.add(calculation()).then(function () {
5824 var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5825 if (! _.isEqual(selection, self.get("selection"))) {
5826 self.set("selection", selection);
5831 * :deprecated: this feature will probably be removed with OpenERP v8
5833 get_distant_fields: function() {
5835 if (! this.options.fold_field) {
5836 this.distant_fields = {}
5838 if (this.distant_fields) {
5839 return $.when(this.distant_fields);
5841 return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
5842 self.distant_fields = fields;
5846 on_click_stage: function (ev) {
5848 var $li = $(ev.currentTarget);
5850 if (this.field.type == "many2one") {
5851 val = parseInt($li.data("id"), 10);
5854 val = $li.data("id");
5856 if (val != self.get('value')) {
5857 this.view.recursive_save().done(function() {
5859 change[self.name] = val;
5860 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5868 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5869 template: "FieldMonetary",
5870 widget_class: 'oe_form_field_float oe_form_field_monetary',
5872 this._super.apply(this, arguments);
5873 this.set({"currency": false});
5874 if (this.options.currency_field) {
5875 this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5876 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5879 this.on("change:currency", this, this.get_currency_info);
5880 this.get_currency_info();
5881 this.ci_dm = new instance.web.DropMisordered();
5884 var tmp = this._super();
5885 this.on("change:currency_info", this, this.reinitialize);
5888 get_currency_info: function() {
5890 if (this.get("currency") === false) {
5891 this.set({"currency_info": null});
5894 return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5895 .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5896 self.set({"currency_info": res});
5899 parse_value: function(val, def) {
5900 return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5902 format_value: function(val, def) {
5903 return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5908 This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
5909 record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
5910 will be added to the relation.
5912 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5913 className: "oe_form_many2many_checkboxes",
5915 this._super.apply(this, arguments);
5916 this.set("value", {});
5917 this.set("records", []);
5918 this.field_manager.on("view_content_has_changed", this, function() {
5919 var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
5920 if (! _.isEqual(domain, this.get("domain"))) {
5921 this.set("domain", domain);
5924 this.records_orderer = new instance.web.DropMisordered();
5926 initialize_field: function() {
5927 instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
5928 this.on("change:domain", this, this.query_records);
5929 this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
5930 this.on("change:records", this, this.render_value);
5932 query_records: function() {
5934 var model = new openerp.Model(openerp.session, this.field.relation);
5935 this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
5936 return model.call("name_get", [record_ids] , {"context": self.build_context()});
5937 })).then(function(res) {
5938 self.set("records", res);
5941 render_value: function() {
5942 this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
5943 var inputs = this.$("input");
5944 inputs.change(_.bind(this.from_dom, this));
5945 if (this.get("effective_readonly"))
5946 inputs.attr("disabled", "true");
5948 from_dom: function() {
5950 this.$("input").each(function() {
5952 new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
5954 if (! _.isEqual(new_value, this.get("value")))
5955 this.internal_set_value(new_value);
5957 set_value: function(value) {
5958 value = value || [];
5959 if (value.length >= 1 && value[0] instanceof Array) {
5960 value = value[0][2];
5963 _.each(value, function(el) {
5964 formatted[JSON.stringify(el)] = true;
5966 this._super(formatted);
5968 get_value: function() {
5969 var value = _.filter(_.keys(this.get("value")), function(el) {
5970 return this.get("value")[el];
5972 value = _.map(value, function(el) {
5973 return JSON.parse(el);
5975 return [commands.replace_with(value)];
5980 This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
5981 "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
5982 action on the model of the relation and show only the linked records.
5986 * 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
5987 to display (or False to take the default one) and the second element is the type of the view. Defaults to
5988 [[false, "tree"], [false, "form"]] .
5990 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5991 className: "oe_form_x2many_counter",
5993 this._super.apply(this, arguments);
5994 this.set("value", []);
5995 _.defaults(this.options, {
5996 "views": [[false, "tree"], [false, "form"]],
5999 render_value: function() {
6000 var text = _.str.sprintf("%d %s", this.val().length, this.string);
6001 this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6002 this.$("a").click(_.bind(this.go_to, this));
6005 return this.view.recursive_save().then(_.bind(function() {
6006 var val = this.val();
6008 if (this.field.type === "one2many") {
6009 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6011 var domain = [["id", "in", val]];
6012 return this.do_action({
6013 type: 'ir.actions.act_window',
6015 res_model: this.field.relation,
6016 views: this.options.views,
6024 var value = this.get("value") || [];
6025 if (value.length >= 1 && value[0] instanceof Array) {
6026 value = value[0][2];
6033 This widget is intended to be used on stat button numeric fields. It will display
6034 the value many2many and one2many. It is a read-only field that will
6035 display a simple string "<value of field> <label of the field>"
6037 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6038 is_field_number: true,
6040 this._super.apply(this, arguments);
6041 this.internal_set_value(0);
6043 set_value: function(value_) {
6044 if (value_ === false || value_ === undefined) {
6047 this._super.apply(this, [value_]);
6049 render_value: function() {
6051 value: this.get("value") || 0,
6053 if (! this.node.attrs.nolabel) {
6054 options.text = this.string
6056 this.$el.html(QWeb.render("StatInfo", options));
6063 * Registry of form fields, called by :js:`instance.web.FormView`.
6065 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6066 * will substitute to the <field> tags as defined in OpenERP's views.
6068 instance.web.form.widgets = new instance.web.Registry({
6069 'char' : 'instance.web.form.FieldChar',
6070 'id' : 'instance.web.form.FieldID',
6071 'email' : 'instance.web.form.FieldEmail',
6072 'url' : 'instance.web.form.FieldUrl',
6073 'text' : 'instance.web.form.FieldText',
6074 'html' : 'instance.web.form.FieldTextHtml',
6075 'char_domain': 'instance.web.form.FieldCharDomain',
6076 'date' : 'instance.web.form.FieldDate',
6077 'datetime' : 'instance.web.form.FieldDatetime',
6078 'selection' : 'instance.web.form.FieldSelection',
6079 'radio' : 'instance.web.form.FieldRadio',
6080 'many2one' : 'instance.web.form.FieldMany2One',
6081 'many2onebutton' : 'instance.web.form.Many2OneButton',
6082 'many2many' : 'instance.web.form.FieldMany2Many',
6083 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6084 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6085 'one2many' : 'instance.web.form.FieldOne2Many',
6086 'one2many_list' : 'instance.web.form.FieldOne2Many',
6087 'reference' : 'instance.web.form.FieldReference',
6088 'boolean' : 'instance.web.form.FieldBoolean',
6089 'float' : 'instance.web.form.FieldFloat',
6090 'percentpie': 'instance.web.form.FieldPercentPie',
6091 'barchart': 'instance.web.form.FieldBarChart',
6092 'integer': 'instance.web.form.FieldFloat',
6093 'float_time': 'instance.web.form.FieldFloat',
6094 'progressbar': 'instance.web.form.FieldProgressBar',
6095 'image': 'instance.web.form.FieldBinaryImage',
6096 'binary': 'instance.web.form.FieldBinaryFile',
6097 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6098 'statusbar': 'instance.web.form.FieldStatus',
6099 'monetary': 'instance.web.form.FieldMonetary',
6100 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6101 'x2many_counter': 'instance.web.form.X2ManyCounter',
6102 'statinfo': 'instance.web.form.StatInfo',
6106 * Registry of widgets usable in the form view that can substitute to any possible
6107 * tags defined in OpenERP's form views.
6109 * Every referenced class should extend FormWidget.
6111 instance.web.form.tags = new instance.web.Registry({
6112 'button' : 'instance.web.form.WidgetButton',
6115 instance.web.form.custom_widgets = new instance.web.Registry({
6120 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: