d140a9adb6288dd2b22c429986e4110ac45a393c
[odoo/odoo.git] / addons / web / static / src / js / view_form.js
1 openerp.web.form = function (instance) {
2 var _t = instance.web._t,
3    _lt = instance.web._lt;
4 var QWeb = instance.web.qweb;
5
6 /** @namespace */
7 instance.web.form = {};
8
9 /**
10  * Interface implemented by the form view or any other object
11  * able to provide the features necessary for the fields to work.
12  *
13  * Properties:
14  *     - display_invalid_fields : if true, all fields where is_valid() return true should
15  *     be displayed as invalid.
16  *     - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
17  * Events:
18  *     - view_content_has_changed : when the values of the fields have changed. When
19  *     this event is triggered all fields should reprocess their modifiers.
20  *     - field_changed:<field_name> : when the value of a field change, an event is triggered
21  *     named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
22  *     This event is not related to the on_change mechanism of OpenERP and is always called
23  *     when the value of a field is setted or changed. This event is only triggered when the
24  *     value of the field is syntactically valid, but it can be triggered when the value
25  *     is sematically invalid (ie, when a required field is false). It is possible that an event
26  *     about a precise field is never triggered even if that field exists in the view, in that
27  *     case the value of the field is assumed to be false.
28  */
29 instance.web.form.FieldManagerMixin = {
30     /**
31      * Must return the asked field as in fields_get.
32      */
33     get_field_desc: function(field_name) {},
34     /**
35      * Returns the current value of a field present in the view. See the get_value() method
36      * method in FieldInterface for further information.
37      */
38     get_field_value: function(field_name) {},
39     /**
40     Gives new values for the fields contained in the view. The new values could not be setted
41     right after the call to this method. Setting new values can trigger on_changes.
42
43     @param (dict) values A dictonnary with key = field name and value = new value.
44     @return (Deferred) Is resolved after all the values are setted.
45     */
46     set_values: function(values) {},
47     /**
48     Computes an OpenERP domain.
49
50     @param (list) expression An OpenERP domain.
51     @return (boolean) The computed value of the domain.
52     */
53     compute_domain: function(expression) {},
54     /**
55     Builds an evaluation context for the resolution of the fields' contexts. Please note
56     the field are only supposed to use this context to evualuate their own, they should not
57     extend it.
58
59     @return (CompoundContext) An OpenERP context.
60     */
61     build_eval_context: function() {},
62 };
63
64 instance.web.views.add('form', 'instance.web.FormView');
65 /**
66  * Properties:
67  *      - actual_mode: always "view", "edit" or "create". Read-only property. Determines
68  *      the mode used by the view.
69  */
70 instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
71     /**
72      * Indicates that this view is not searchable, and thus that no search
73      * view should be displayed (if there is one active).
74      */
75     searchable: false,
76     template: "FormView",
77     display_name: _lt('Form'),
78     view_type: "form",
79     /**
80      * @constructs instance.web.FormView
81      * @extends instance.web.View
82      *
83      * @param {instance.web.Session} session the current openerp session
84      * @param {instance.web.DataSet} dataset the dataset this view will work with
85      * @param {String} view_id the identifier of the OpenERP view object
86      * @param {Object} options
87      *                  - resize_textareas : [true|false|max_height]
88      *
89      * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
90      */
91     init: function(parent, dataset, view_id, options) {
92         var self = this;
93         this._super(parent);
94         this.ViewManager = parent;
95         this.set_default_options(options);
96         this.dataset = dataset;
97         this.model = dataset.model;
98         this.view_id = view_id || false;
99         this.fields_view = {};
100         this.fields = {};
101         this.fields_order = [];
102         this.datarecord = {};
103         this.default_focus_field = null;
104         this.default_focus_button = null;
105         this.fields_registry = instance.web.form.widgets;
106         this.tags_registry = instance.web.form.tags;
107         this.widgets_registry = instance.web.form.custom_widgets;
108         this.has_been_loaded = $.Deferred();
109         this.translatable_fields = [];
110         _.defaults(this.options, {
111             "not_interactible_on_create": false,
112             "initial_mode": "view",
113             "disable_autofocus": false,
114             "footer_to_buttons": false,
115         });
116         this.is_initialized = $.Deferred();
117         this.mutating_mutex = new $.Mutex();
118         this.on_change_list = [];
119         this.save_list = [];
120         this.render_value_defs = [];
121         this.reload_mutex = new $.Mutex();
122         this.__clicked_inside = false;
123         this.__blur_timeout = null;
124         this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
125         self.set({actual_mode: self.options.initial_mode});
126         this.has_been_loaded.done(function() {
127             self.on("change:actual_mode", self, self.check_actual_mode);
128             self.check_actual_mode();
129             self.on("change:actual_mode", self, self.init_pager);
130             self.init_pager();
131         });
132         self.on("load_record", self, self.load_record);
133         instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
134             if (!this.can_be_discarded()) {
135                 e.preventDefault();
136             }
137         });
138     },
139     view_loading: function(r) {
140         return this.load_form(r);
141     },
142     destroy: function() {
143         _.each(this.get_widgets(), function(w) {
144             w.off('focused blurred');
145             w.destroy();
146         });
147         if (this.$el) {
148             this.$el.off('.formBlur');
149         }
150         this._super();
151     },
152     load_form: function(data) {
153         var self = this;
154         if (!data) {
155             throw new Error(_t("No data provided."));
156         }
157         if (this.arch) {
158             throw "Form view does not support multiple calls to load_form";
159         }
160         this.fields_order = [];
161         this.fields_view = data;
162
163         this.rendering_engine.set_fields_registry(this.fields_registry);
164         this.rendering_engine.set_tags_registry(this.tags_registry);
165         this.rendering_engine.set_widgets_registry(this.widgets_registry);
166         this.rendering_engine.set_fields_view(data);
167         var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
168         this.rendering_engine.render_to($dest);
169
170         this.$el.on('mousedown.formBlur', function () {
171             self.__clicked_inside = true;
172         });
173
174         this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
175         if (this.options.$buttons) {
176             this.$buttons.appendTo(this.options.$buttons);
177         } else {
178             this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
179         }
180         this.$buttons.on('click', '.oe_form_button_create',
181                          this.guard_active(this.on_button_create));
182         this.$buttons.on('click', '.oe_form_button_edit',
183                          this.guard_active(this.on_button_edit));
184         this.$buttons.on('click', '.oe_form_button_save',
185                          this.guard_active(this.on_button_save));
186         this.$buttons.on('click', '.oe_form_button_cancel',
187                          this.guard_active(this.on_button_cancel));
188         if (this.options.footer_to_buttons) {
189             this.$el.find('footer').appendTo(this.$buttons);
190         }
191
192         this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
193         if (!this.sidebar && this.options.$sidebar) {
194             this.sidebar = new instance.web.Sidebar(this);
195             this.sidebar.appendTo(this.$sidebar);
196             if (this.fields_view.toolbar) {
197                 this.sidebar.add_toolbar(this.fields_view.toolbar);
198             }
199             this.sidebar.add_items('other', _.compact([
200                 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
201                 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
202             ]));
203         }
204
205         this.has_been_loaded.resolve();
206
207         // Add bounce effect on button 'Edit' when click on readonly page view.
208         this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
209             if(self.get("actual_mode") == "view") {
210                 var $button = self.options.$buttons.find(".oe_form_button_edit");
211                 $button.openerpBounce();
212                 e.stopPropagation();
213                 instance.web.bus.trigger('click', e);
214             }
215         });
216         //bounce effect on red button when click on statusbar.
217         this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
218             if((self.get("actual_mode") == "view")) {
219                 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
220                 $button.openerpBounce();
221                 e.stopPropagation();
222             }
223          });
224         this.trigger('form_view_loaded', data);
225         return $.when();
226     },
227     widgetFocused: function() {
228         // Clear click flag if used to focus a widget
229         this.__clicked_inside = false;
230         if (this.__blur_timeout) {
231             clearTimeout(this.__blur_timeout);
232             this.__blur_timeout = null;
233         }
234     },
235     widgetBlurred: function() {
236         if (this.__clicked_inside) {
237             // clicked in an other section of the form (than the currently
238             // focused widget) => just ignore the blurring entirely?
239             this.__clicked_inside = false;
240             return;
241         }
242         var self = this;
243         // clear timeout, if any
244         this.widgetFocused();
245         this.__blur_timeout = setTimeout(function () {
246             self.trigger('blurred');
247         }, 0);
248     },
249
250     do_load_state: function(state, warm) {
251         if (state.id && this.datarecord.id != state.id) {
252             if (this.dataset.get_id_index(state.id) === null) {
253                 this.dataset.ids.push(state.id);
254             }
255             this.dataset.select_id(state.id);
256             this.do_show({ reload: warm });
257         }
258     },
259     /**
260      *
261      * @param {Object} [options]
262      * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
263      * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
264      * @return {$.Deferred}
265      */
266     do_show: function (options) {
267         var self = this;
268         options = options || {};
269         if (this.sidebar) {
270             this.sidebar.$el.show();
271         }
272         if (this.$buttons) {
273             this.$buttons.show();
274         }
275         this.$el.show().css({
276             opacity: '0',
277             filter: 'alpha(opacity = 0)'
278         });
279         this.$el.add(this.$buttons).removeClass('oe_form_dirty');
280
281         var shown = this.has_been_loaded;
282         if (options.reload !== false) {
283             shown = shown.then(function() {
284                 if (self.dataset.index === null) {
285                     // null index means we should start a new record
286                     return self.on_button_new();
287                 }
288                 var fields = _.keys(self.fields_view.fields);
289                 fields.push('display_name');
290                 return self.dataset.read_index(fields, {
291                     context: { 'bin_size': true, 'future_display_name' : true }
292                 }).then(function(r) {
293                     self.trigger('load_record', r);
294                 });
295             });
296         }
297         return shown.then(function() {
298             self._actualize_mode(options.mode || self.options.initial_mode);
299             self.$el.css({
300                 opacity: '1',
301                 filter: 'alpha(opacity = 100)'
302             });
303         });
304     },
305     do_hide: function () {
306         if (this.sidebar) {
307             this.sidebar.$el.hide();
308         }
309         if (this.$buttons) {
310             this.$buttons.hide();
311         }
312         if (this.$pager) {
313             this.$pager.hide();
314         }
315         this._super();
316     },
317     load_record: function(record) {
318         var self = this, set_values = [];
319         if (!record) {
320             this.set({ 'title' : undefined });
321             this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
322             return $.Deferred().reject();
323         }
324         this.datarecord = record;
325         this._actualize_mode();
326         this.set({ 'title' : record.id ? record.display_name : _t("New") });
327
328         _(this.fields).each(function (field, f) {
329             field._dirty_flag = false;
330             field._inhibit_on_change_flag = true;
331             var result = field.set_value(self.datarecord[f] || false);
332             field._inhibit_on_change_flag = false;
333             set_values.push(result);
334         });
335         return $.when.apply(null, set_values).then(function() {
336             if (!record.id) {
337                 // New record: Second pass in order to trigger the onchanges
338                 // respecting the fields order defined in the view
339                 _.each(self.fields_order, function(field_name) {
340                     if (record[field_name] !== undefined) {
341                         var field = self.fields[field_name];
342                         field._dirty_flag = true;
343                         self.do_onchange(field);
344                     }
345                 });
346             }
347             self.on_form_changed();
348             self.rendering_engine.init_fields();
349             self.is_initialized.resolve();
350             self.do_update_pager(record.id == null);
351             if (self.sidebar) {
352                self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
353             }
354             if (record.id) {
355                 self.do_push_state({id:record.id});
356             } else {
357                 self.do_push_state({});
358             }
359             self.$el.add(self.$buttons).removeClass('oe_form_dirty');
360             self.autofocus();
361         });
362     },
363     /**
364      * Loads and sets up the default values for the model as the current
365      * record
366      *
367      * @return {$.Deferred}
368      */
369     load_defaults: function () {
370         var self = this;
371         var keys = _.keys(this.fields_view.fields);
372         if (keys.length) {
373             return this.dataset.default_get(keys).then(function(r) {
374                 self.trigger('load_record', r);
375             });
376         }
377         return self.trigger('load_record', {});
378     },
379     on_form_changed: function() {
380         this.trigger("view_content_has_changed");
381     },
382     do_notify_change: function() {
383         this.$el.add(this.$buttons).addClass('oe_form_dirty');
384     },
385     execute_pager_action: function(action) {
386         if (this.can_be_discarded()) {
387             switch (action) {
388                 case 'first':
389                     this.dataset.index = 0;
390                     break;
391                 case 'previous':
392                     this.dataset.previous();
393                     break;
394                 case 'next':
395                     this.dataset.next();
396                     break;
397                 case 'last':
398                     this.dataset.index = this.dataset.ids.length - 1;
399                     break;
400             }
401             this.reload();
402             this.trigger('pager_action_executed');
403         }
404     },
405     init_pager: function() {
406         var self = this;
407         if (this.$pager)
408             this.$pager.remove();
409         if (this.get("actual_mode") === "create")
410             return;
411         this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
412         if (this.options.$pager) {
413             this.$pager.appendTo(this.options.$pager);
414         } else {
415             this.$el.find('.oe_form_pager').replaceWith(this.$pager);
416         }
417         this.$pager.on('click','a[data-pager-action]',function() {
418             var action = $(this).data('pager-action');
419             self.execute_pager_action(action);
420         });
421         this.do_update_pager();
422     },
423     do_update_pager: function(hide_index) {
424         this.$pager.toggle(this.dataset.ids.length > 1);
425         if (hide_index) {
426             $(".oe_form_pager_state", this.$pager).html("");
427         } else {
428             $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
429         }
430     },
431     parse_on_change: function (on_change, widget) {
432         var self = this;
433         var onchange = _.str.trim(on_change);
434         var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
435         if (!call) {
436             throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
437         }
438
439         var method = call[1];
440         if (!_.str.trim(call[2])) {
441             return {method: method, args: []}
442         }
443
444         var argument_replacement = {
445             'False': function () {return false;},
446             'True': function () {return true;},
447             'None': function () {return null;},
448             'context': function () {
449                 return new instance.web.CompoundContext(
450                         self.dataset.get_context(),
451                         widget.build_context() ? widget.build_context() : {});
452             }
453         };
454         var parent_fields = null;
455         var args = _.map(call[2].split(','), function (a, i) {
456             var field = _.str.trim(a);
457
458             // literal constant or context
459             if (field in argument_replacement) {
460                 return argument_replacement[field]();
461             }
462             // literal number
463             if (/^-?\d+(\.\d+)?$/.test(field)) {
464                 return Number(field);
465             }
466             // form field
467             if (self.fields[field]) {
468                 var value_ = self.fields[field].get_value();
469                 return value_ == null ? false : value_;
470             }
471             // parent field
472             var splitted = field.split('.');
473             if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
474                 if (parent_fields === null) {
475                     parent_fields = self.dataset.parent_view.get_fields_values();
476                 }
477                 var p_val = parent_fields[_.str.trim(splitted[1])];
478                 if (p_val !== undefined) {
479                     return p_val == null ? false : p_val;
480                 }
481             }
482             // string literal
483             var first_char = field[0], last_char = field[field.length-1];
484             if ((first_char === '"' && last_char === '"')
485                 || (first_char === "'" && last_char === "'")) {
486                 return field.slice(1, -1);
487             }
488
489             throw new Error("Could not get field with name '" + field +
490                             "' for onchange '" + onchange + "'");
491         });
492
493         return {
494             method: method,
495             args: args
496         };
497     },
498     do_onchange: function(widget, processed) {
499         var self = this;
500         this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
501         return this._process_operations();
502     },
503     _process_onchange: function(on_change_obj) {
504         var self = this;
505         var widget = on_change_obj.widget;
506         var processed = on_change_obj.processed;
507         try {
508             var def;
509             processed = processed || [];
510             processed.push(widget.name);
511             var on_change = widget.node.attrs.on_change;
512             if (on_change) {
513                 var change_spec = self.parse_on_change(on_change, widget);
514                 var ids = [];
515                 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
516                     // In case of a o2m virtual id, we should pass an empty ids list
517                     ids.push(self.datarecord.id);
518                 }
519                 def = self.alive(new instance.web.Model(self.dataset.model).call(
520                     change_spec.method, [ids].concat(change_spec.args)));
521             } else {
522                 def = $.when({});
523             }
524             return def.then(function(response) {
525                 if (widget.field['change_default']) {
526                     var fieldname = widget.name;
527                     var value_;
528                     if (response.value && (fieldname in response.value)) {
529                         // Use value from onchange if onchange executed
530                         value_ = response.value[fieldname];
531                     } else {
532                         // otherwise get form value for field
533                         value_ = self.fields[fieldname].get_value();
534                     }
535                     var condition = fieldname + '=' + value_;
536
537                     if (value_) {
538                         return self.alive(new instance.web.Model('ir.values').call(
539                             'get_defaults', [self.model, condition]
540                         )).then(function (results) {
541                             if (!results.length) {
542                                 return response;
543                             }
544                             if (!response.value) {
545                                 response.value = {};
546                             }
547                             for(var i=0; i<results.length; ++i) {
548                                 // [whatever, key, value]
549                                 var triplet = results[i];
550                                 response.value[triplet[1]] = triplet[2];
551                             }
552                             return response;
553                         });
554                     }
555                 }
556                 return response;
557             }).then(function(response) {
558                 return self.on_processed_onchange(response, processed);
559             });
560         } catch(e) {
561             console.error(e);
562             instance.webclient.crashmanager.show_message(e);
563             return $.Deferred().reject();
564         }
565     },
566     on_processed_onchange: function(result, processed) {
567         try {
568         if (result.value) {
569             this._internal_set_values(result.value, processed);
570         }
571         if (!_.isEmpty(result.warning)) {
572             instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
573                 title:result.warning.title,
574                 modal: true,
575                 buttons: [
576                     {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
577                 ]
578             });
579         }
580
581         var fields = this.fields;
582         _(result.domain).each(function (domain, fieldname) {
583             var field = fields[fieldname];
584             if (!field) { return; }
585             field.node.attrs.domain = domain;
586         });
587
588         return $.Deferred().resolve();
589         } catch(e) {
590             console.error(e);
591             instance.webclient.crashmanager.show_message(e);
592             return $.Deferred().reject();
593         }
594     },
595     _process_operations: function() {
596         var self = this;
597         return this.mutating_mutex.exec(function() {
598             function iterate() {
599                 var on_change_obj = self.on_change_list.shift();
600                 if (on_change_obj) {
601                     return self._process_onchange(on_change_obj).then(function() {
602                         return iterate();
603                     });
604                 }
605                 var defs = [];
606                 _.each(self.fields, function(field) {
607                     defs.push(field.commit_value());
608                 });
609                 var args = _.toArray(arguments);
610                 return $.when.apply($, defs).then(function() {
611                     if (self.on_change_list.length !== 0) {
612                         return iterate();
613                     }
614                     var save_obj = self.save_list.pop();
615                     if (save_obj) {
616                         return self._process_save(save_obj).then(function() {
617                             save_obj.ret = _.toArray(arguments);
618                             return iterate();
619                         }, function() {
620                             save_obj.error = true;
621                         });
622                     }
623                     return $.when();
624                 }).fail(function() {
625                     self.save_list.pop();
626                     return $.when();
627                 });
628             };
629             return iterate();
630         });
631     },
632     _internal_set_values: function(values, exclude) {
633         exclude = exclude || [];
634         for (var f in values) {
635             if (!values.hasOwnProperty(f)) { continue; }
636             var field = this.fields[f];
637             // If field is not defined in the view, just ignore it
638             if (field) {
639                 var value_ = values[f];
640                 if (field.get_value() != value_) {
641                     field._inhibit_on_change_flag = true;
642                     field.set_value(value_);
643                     field._inhibit_on_change_flag = false;
644                     field._dirty_flag = true;
645                     if (!_.contains(exclude, field.name)) {
646                         this.do_onchange(field, exclude);
647                     }
648                 }
649             }
650         }
651         this.on_form_changed();
652     },
653     set_values: function(values) {
654         var self = this;
655         return this.mutating_mutex.exec(function() {
656             self._internal_set_values(values);
657         });
658     },
659     /**
660      * Ask the view to switch to view mode if possible. The view may not do it
661      * if the current record is not yet saved. It will then stay in create mode.
662      */
663     to_view_mode: function() {
664         this._actualize_mode("view");
665     },
666     /**
667      * Ask the view to switch to edit mode if possible. The view may not do it
668      * if the current record is not yet saved. It will then stay in create mode.
669      */
670     to_edit_mode: function() {
671         this._actualize_mode("edit");
672     },
673     /**
674      * Ask the view to switch to a precise mode if possible. The view is free to
675      * not respect this command if the state of the dataset is not compatible with
676      * the new mode. For example, it is not possible to switch to edit mode if
677      * the current record is not yet saved in database.
678      *
679      * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
680      * undefined the view will test the actual mode to check if it is still consistent
681      * with the dataset state.
682      */
683     _actualize_mode: function(switch_to) {
684         var mode = switch_to || this.get("actual_mode");
685         if (! this.datarecord.id) {
686             mode = "create";
687         } else if (mode === "create") {
688             mode = "edit";
689         }
690         this.render_value_defs = [];
691         this.set({actual_mode: mode});
692     },
693     check_actual_mode: function(source, options) {
694         var self = this;
695         if(this.get("actual_mode") === "view") {
696             self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
697             self.$buttons.find('.oe_form_buttons_edit').hide();
698             self.$buttons.find('.oe_form_buttons_view').show();
699             self.$sidebar.show();
700         } else {
701             self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
702             self.$buttons.find('.oe_form_buttons_edit').show();
703             self.$buttons.find('.oe_form_buttons_view').hide();
704             self.$sidebar.hide();
705             this.autofocus();
706         }
707     },
708     autofocus: function() {
709         if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
710             var fields_order = this.fields_order.slice(0);
711             if (this.default_focus_field) {
712                 fields_order.unshift(this.default_focus_field.name);
713             }
714             for (var i = 0; i < fields_order.length; i += 1) {
715                 var field = this.fields[fields_order[i]];
716                 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
717                     if (field.focus() !== false) {
718                         break;
719                     }
720                 }
721             }
722         }
723     },
724     on_button_save: function(e) {
725         var self = this;
726         $(e.target).attr("disabled", true);
727         return this.save().done(function(result) {
728             self.trigger("save", result);
729             self.reload().then(function() {
730                 self.to_view_mode();
731                 var parent = self.ViewManager.ActionManager.getParent();
732                 if(parent){
733                     parent.menu.do_reload_needaction();
734                 }
735             });
736         }).always(function(){
737             $(e.target).attr("disabled", false);
738         });
739     },
740     on_button_cancel: function(event) {
741         var self = this;
742         if (this.can_be_discarded()) {
743             if (this.get('actual_mode') === 'create') {
744                 this.trigger('history_back');
745             } else {
746                 this.to_view_mode();
747                 $.when.apply(null, this.render_value_defs).then(function(){
748                     self.trigger('load_record', self.datarecord);
749                 });
750             }
751         }
752         this.trigger('on_button_cancel');
753         return false;
754     },
755     on_button_new: function() {
756         var self = this;
757         this.to_edit_mode();
758         return $.when(this.has_been_loaded).then(function() {
759             if (self.can_be_discarded()) {
760                 return self.load_defaults();
761             }
762         });
763     },
764     on_button_edit: function() {
765         return this.to_edit_mode();
766     },
767     on_button_create: function() {
768         this.dataset.index = null;
769         this.do_show();
770     },
771     on_button_duplicate: function() {
772         var self = this;
773         return this.has_been_loaded.then(function() {
774             return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
775                 self.record_created(new_id);
776                 self.to_edit_mode();
777             });
778         });
779     },
780     on_button_delete: function() {
781         var self = this;
782         var def = $.Deferred();
783         this.has_been_loaded.done(function() {
784             if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
785                 self.dataset.unlink([self.datarecord.id]).done(function() {
786                     if (self.dataset.size()) {
787                         self.execute_pager_action('next');
788                     } else {
789                         self.do_action('history_back');
790                     }
791                     def.resolve();
792                 });
793             } else {
794                 $.async_when().done(function () {
795                     def.reject();
796                 })
797             }
798         });
799         return def.promise();
800     },
801     can_be_discarded: function() {
802         if (this.$el.is('.oe_form_dirty')) {
803             if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
804                 return false;
805             }
806             this.$el.removeClass('oe_form_dirty');
807         }
808         return true;
809     },
810     /**
811      * Triggers saving the form's record. Chooses between creating a new
812      * record or saving an existing one depending on whether the record
813      * already has an id property.
814      *
815      * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
816      * record, should that record be inserted at the start of the dataset (by
817      * default, records are added at the end)
818      */
819     save: function(prepend_on_create) {
820         var self = this;
821         var save_obj = {prepend_on_create: prepend_on_create, ret: null};
822         this.save_list.push(save_obj);
823         return this._process_operations().then(function() {
824             if (save_obj.error)
825                 return $.Deferred().reject();
826             return $.when.apply($, save_obj.ret);
827         }).done(function() {
828             self.$el.removeClass('oe_form_dirty');
829         });
830     },
831     _process_save: function(save_obj) {
832         var self = this;
833         var prepend_on_create = save_obj.prepend_on_create;
834         try {
835             var form_invalid = false,
836                 values = {},
837                 first_invalid_field = null,
838                 readonly_values = {};
839             for (var f in self.fields) {
840                 if (!self.fields.hasOwnProperty(f)) { continue; }
841                 f = self.fields[f];
842                 if (!f.is_valid()) {
843                     form_invalid = true;
844                     if (!first_invalid_field) {
845                         first_invalid_field = f;
846                     }
847                 } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
848                     // Special case 'id' field, do not save this field
849                     // on 'create' : save all non readonly fields
850                     // on 'edit' : save non readonly modified fields
851                     if (!f.get("readonly")) {
852                         values[f.name] = f.get_value();
853                     } else {
854                         readonly_values[f.name] = f.get_value();
855                     }
856                 }
857             }
858             if (form_invalid) {
859                 self.set({'display_invalid_fields': true});
860                 first_invalid_field.focus();
861                 self.on_invalid();
862                 return $.Deferred().reject();
863             } else {
864                 self.set({'display_invalid_fields': false});
865                 var save_deferral;
866                 if (!self.datarecord.id) {
867                     // Creation save
868                     save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
869                         return self.record_created(r, prepend_on_create);
870                     }, null);
871                 } else if (_.isEmpty(values)) {
872                     // Not dirty, noop save
873                     save_deferral = $.Deferred().resolve({}).promise();
874                 } else {
875                     // Write save
876                     save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
877                         return self.record_saved(r);
878                     }, null);
879                 }
880                 return save_deferral;
881             }
882         } catch (e) {
883             console.error(e);
884             return $.Deferred().reject();
885         }
886     },
887     on_invalid: function() {
888         var warnings = _(this.fields).chain()
889             .filter(function (f) { return !f.is_valid(); })
890             .map(function (f) {
891                 return _.str.sprintf('<li>%s</li>',
892                     _.escape(f.string));
893             }).value();
894         warnings.unshift('<ul>');
895         warnings.push('</ul>');
896         this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
897     },
898     /**
899      * Reload the form after saving
900      *
901      * @param {Object} r result of the write function.
902      */
903     record_saved: function(r) {
904         this.trigger('record_saved', r);
905         if (!r) {
906             // should not happen in the server, but may happen for internal purpose
907             return $.Deferred().reject();
908         }
909         return r;
910     },
911     /**
912      * Updates the form' dataset to contain the new record:
913      *
914      * * Adds the newly created record to the current dataset (at the end by
915      *   default)
916      * * Selects that record (sets the dataset's index to point to the new
917      *   record's id).
918      * * Updates the pager and sidebar displays
919      *
920      * @param {Object} r
921      * @param {Boolean} [prepend_on_create=false] adds the newly created record
922      * at the beginning of the dataset instead of the end
923      */
924     record_created: function(r, prepend_on_create) {
925         var self = this;
926         if (!r) {
927             // should not happen in the server, but may happen for internal purpose
928             this.trigger('record_created', r);
929             return $.Deferred().reject();
930         } else {
931             this.datarecord.id = r;
932             if (!prepend_on_create) {
933                 this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
934                 this.dataset.index = this.dataset.ids.length - 1;
935             } else {
936                 this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
937                 this.dataset.index = 0;
938             }
939             this.do_update_pager();
940             if (this.sidebar) {
941                 this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
942             }
943             //openerp.log("The record has been created with id #" + this.datarecord.id);
944             return $.when(this.reload()).then(function () {
945                 self.trigger('record_created', r);
946                 return _.extend(r, {created: true});
947             });
948         }
949     },
950     on_action: function (action) {
951         console.debug('Executing action', action);
952     },
953     reload: function() {
954         var self = this;
955         return this.reload_mutex.exec(function() {
956             if (self.dataset.index == null) {
957                 self.trigger("previous_view");
958                 return $.Deferred().reject().promise();
959             }
960             if (self.dataset.index == null || self.dataset.index < 0) {
961                 return $.when(self.on_button_new());
962             } else {
963                 var fields = _.keys(self.fields_view.fields);
964                 fields.push('display_name');
965                 return self.dataset.read_index(fields,
966                     {
967                         context: {
968                             'bin_size': true,
969                             'future_display_name': true
970                         },
971                         check_access_rule: true
972                     }).then(function(r) {
973                         self.trigger('load_record', r);
974                     }).fail(function (){
975                         self.do_action('history_back');
976                     });
977             }
978         });
979     },
980     get_widgets: function() {
981         return _.filter(this.getChildren(), function(obj) {
982             return obj instanceof instance.web.form.FormWidget;
983         });
984     },
985     get_fields_values: function() {
986         var values = {};
987         var ids = this.get_selected_ids();
988         values["id"] = ids.length > 0 ? ids[0] : false;
989         _.each(this.fields, function(value_, key) {
990             values[key] = value_.get_value();
991         });
992         return values;
993     },
994     get_selected_ids: function() {
995         var id = this.dataset.ids[this.dataset.index];
996         return id ? [id] : [];
997     },
998     recursive_save: function() {
999         var self = this;
1000         return $.when(this.save()).then(function(res) {
1001             if (self.dataset.parent_view)
1002                 return self.dataset.parent_view.recursive_save();
1003         });
1004     },
1005     recursive_reload: function() {
1006         var self = this;
1007         var pre = $.when();
1008         if (self.dataset.parent_view)
1009                 pre = self.dataset.parent_view.recursive_reload();
1010         return pre.then(function() {
1011             return self.reload();
1012         });
1013     },
1014     is_dirty: function() {
1015         return _.any(this.fields, function (value_) {
1016             return value_._dirty_flag;
1017         });
1018     },
1019     is_interactible_record: function() {
1020         var id = this.datarecord.id;
1021         if (!id) {
1022             if (this.options.not_interactible_on_create)
1023                 return false;
1024         } else if (typeof(id) === "string") {
1025             if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
1026                 return false;
1027         }
1028         return true;
1029     },
1030     sidebar_eval_context: function () {
1031         return $.when(this.build_eval_context());
1032     },
1033     open_defaults_dialog: function () {
1034         var self = this;
1035         var display = function (field, value) {
1036             if (field instanceof instance.web.form.FieldSelection) {
1037                 return _(field.values).find(function (option) {
1038                     return option[0] === value;
1039                 })[1];
1040             } else if (field instanceof instance.web.form.FieldMany2One) {
1041                 return field.get_displayed();
1042             }
1043             return value;
1044         }
1045         var fields = _.chain(this.fields)
1046             .map(function (field) {
1047                 var value = field.get_value();
1048                 // ignore fields which are empty, invisible, readonly, o2m
1049                 // or m2m
1050                 if (!value
1051                         || field.get('invisible')
1052                         || field.get("readonly")
1053                         || field.field.type === 'one2many'
1054                         || field.field.type === 'many2many'
1055                         || field.field.type === 'binary'
1056                         || field.password) {
1057                     return false;
1058                 }
1059
1060                 return {
1061                     name: field.name,
1062                     string: field.string,
1063                     value: value,
1064                     displayed: display(field, value),
1065                 }
1066             })
1067             .compact()
1068             .sortBy(function (field) { return field.string; })
1069             .value();
1070         var conditions = _.chain(self.fields)
1071             .filter(function (field) { return field.field.change_default; })
1072             .map(function (field) {
1073                 var value = field.get_value();
1074                 return {
1075                     name: field.name,
1076                     string: field.string,
1077                     value: value,
1078                     displayed: display(field, value),
1079                 }
1080             })
1081             .value();
1082
1083         var d = new instance.web.Dialog(this, {
1084             title: _t("Set Default"),
1085             args: {
1086                 fields: fields,
1087                 conditions: conditions
1088             },
1089             buttons: [
1090                 {text: _t("Close"), click: function () { d.close(); }},
1091                 {text: _t("Save default"), click: function () {
1092                     var $defaults = d.$el.find('#formview_default_fields');
1093                     var field_to_set = $defaults.val();
1094                     if (!field_to_set) {
1095                         $defaults.parent().addClass('oe_form_invalid');
1096                         return;
1097                     }
1098                     var condition = d.$el.find('#formview_default_conditions').val(),
1099                         all_users = d.$el.find('#formview_default_all').is(':checked');
1100                     new instance.web.DataSet(self, 'ir.values').call(
1101                         'set_default', [
1102                             self.dataset.model,
1103                             field_to_set,
1104                             self.fields[field_to_set].get_value(),
1105                             all_users,
1106                             true,
1107                             condition || false
1108                     ]).done(function () { d.close(); });
1109                 }}
1110             ]
1111         });
1112         d.template = 'FormView.set_default';
1113         d.open();
1114     },
1115     register_field: function(field, name) {
1116         this.fields[name] = field;
1117         this.fields_order.push(name);
1118         if (JSON.parse(field.node.attrs.default_focus || "0")) {
1119             this.default_focus_field = field;
1120         }
1121
1122         field.on('focused', null, this.proxy('widgetFocused'))
1123              .on('blurred', null, this.proxy('widgetBlurred'));
1124         if (this.get_field_desc(name).translate) {
1125             this.translatable_fields.push(field);
1126         }
1127         field.on('changed_value', this, function() {
1128             if (field.is_syntax_valid()) {
1129                 this.trigger('field_changed:' + name);
1130             }
1131             if (field._inhibit_on_change_flag) {
1132                 return;
1133             }
1134             field._dirty_flag = true;
1135             if (field.is_syntax_valid()) {
1136                 this.do_onchange(field);
1137                 this.on_form_changed(true);
1138                 this.do_notify_change();
1139             }
1140         });
1141     },
1142     get_field_desc: function(field_name) {
1143         return this.fields_view.fields[field_name];
1144     },
1145     get_field_value: function(field_name) {
1146         return this.fields[field_name].get_value();
1147     },
1148     compute_domain: function(expression) {
1149         return instance.web.form.compute_domain(expression, this.fields);
1150     },
1151     _build_view_fields_values: function() {
1152         var a_dataset = this.dataset;
1153         var fields_values = this.get_fields_values();
1154         var active_id = a_dataset.ids[a_dataset.index];
1155         _.extend(fields_values, {
1156             active_id: active_id || false,
1157             active_ids: active_id ? [active_id] : [],
1158             active_model: a_dataset.model,
1159             parent: {}
1160         });
1161         if (a_dataset.parent_view) {
1162             fields_values.parent = a_dataset.parent_view.get_fields_values();
1163         }
1164         return fields_values;
1165     },
1166     build_eval_context: function() {
1167         var a_dataset = this.dataset;
1168         return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
1169     },
1170 });
1171
1172 /**
1173  * Interface to be implemented by rendering engines for the form view.
1174  */
1175 instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
1176     set_fields_view: function(fields_view) {},
1177     set_fields_registry: function(fields_registry) {},
1178     render_to: function($el) {},
1179 });
1180
1181 /**
1182  * Default rendering engine for the form view.
1183  *
1184  * It is necessary to set the view using set_view() before usage.
1185  */
1186 instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
1187     init: function(view) {
1188         this.view = view;
1189     },
1190     set_fields_view: function(fvg) {
1191         this.fvg = fvg;
1192         this.version = parseFloat(this.fvg.arch.attrs.version);
1193         if (isNaN(this.version)) {
1194             this.version = 6.1;
1195         }
1196     },
1197     set_tags_registry: function(tags_registry) {
1198         this.tags_registry = tags_registry;
1199     },
1200     set_fields_registry: function(fields_registry) {
1201         this.fields_registry = fields_registry;
1202     },
1203     set_widgets_registry: function(widgets_registry) {
1204         this.widgets_registry = widgets_registry;
1205     },
1206     // Backward compatibility tools, current default version: v6.1
1207     process_version: function() {
1208         if (this.version < 7.0) {
1209             this.$form.find('form:first').wrapInner('<group col="4"/>');
1210             this.$form.find('page').each(function() {
1211                 if (!$(this).parents('field').length) {
1212                     $(this).wrapInner('<group col="4"/>');
1213                 }
1214             });
1215         }
1216     },
1217     get_arch_fragment: function() {
1218         var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
1219         // IE won't allow custom button@type and will revert it to spec default : 'submit'
1220         $('button', doc).each(function() {
1221             $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
1222         });
1223         // IE's html parser is also a css parser. How convenient...
1224         $('board', doc).each(function() {
1225             $(this).attr('layout', $(this).attr('style'));
1226         });
1227         return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
1228     },
1229     render_to: function($target) {
1230         var self = this;
1231         this.$target = $target;
1232
1233         this.$form = this.get_arch_fragment();
1234
1235         this.process_version();
1236
1237         this.fields_to_init = [];
1238         this.tags_to_init = [];
1239         this.widgets_to_init = [];
1240         this.labels = {};
1241         this.process(this.$form);
1242
1243         this.$form.appendTo(this.$target);
1244
1245         this.to_replace = [];
1246
1247         _.each(this.fields_to_init, function($elem) {
1248             var name = $elem.attr("name");
1249             if (!self.fvg.fields[name]) {
1250                 throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
1251             }
1252             var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
1253             if (!obj) {
1254                 throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
1255             }
1256             var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1257             var $label = self.labels[$elem.attr("name")];
1258             if ($label) {
1259                 w.set_input_id($label.attr("for"));
1260             }
1261             self.alter_field(w);
1262             self.view.register_field(w, $elem.attr("name"));
1263             self.to_replace.push([w, $elem]);
1264         });
1265         _.each(this.tags_to_init, function($elem) {
1266             var tag_name = $elem[0].tagName.toLowerCase();
1267             var obj = self.tags_registry.get_object(tag_name);
1268             var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1269             self.to_replace.push([w, $elem]);
1270         });
1271         _.each(this.widgets_to_init, function($elem) {
1272             var widget_type = $elem.attr("type");
1273             var obj = self.widgets_registry.get_object(widget_type);
1274             var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
1275             self.to_replace.push([w, $elem]);
1276         });
1277     },
1278     init_fields: function() {
1279         var defs = [];
1280         _.each(this.to_replace, function(el) {
1281             defs.push(el[0].replace(el[1]));
1282         });
1283         this.to_replace = [];
1284         return $.when.apply($, defs);
1285     },
1286     render_element: function(template /* dictionaries */) {
1287         var dicts = [].slice.call(arguments).slice(1);
1288         var dict = _.extend.apply(_, dicts);
1289         dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
1290         return $(QWeb.render(template, dict));
1291     },
1292     alter_field: function(field) {
1293     },
1294     toggle_layout_debugging: function() {
1295         if (!this.$target.has('.oe_layout_debug_cell:first').length) {
1296             this.$target.find('[title]').removeAttr('title');
1297             this.$target.find('.oe_form_group_cell').each(function() {
1298                 var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
1299                 $(this).attr('title', text);
1300             });
1301         }
1302         this.$target.toggleClass('oe_layout_debugging');
1303     },
1304     process: function($tag) {
1305         var self = this;
1306         var tagname = $tag[0].nodeName.toLowerCase();
1307         if (this.tags_registry.contains(tagname)) {
1308             this.tags_to_init.push($tag);
1309             return $tag;
1310         }
1311         var fn = self['process_' + tagname];
1312         if (fn) {
1313             var args = [].slice.call(arguments);
1314             args[0] = $tag;
1315             return fn.apply(self, args);
1316         } else {
1317             // generic tag handling, just process children
1318             $tag.children().each(function() {
1319                 self.process($(this));
1320             });
1321             self.handle_common_properties($tag, $tag);
1322             $tag.removeAttr("modifiers");
1323             return $tag;
1324         }
1325     },
1326     process_widget: function($widget) {
1327         this.widgets_to_init.push($widget);
1328         return $widget;
1329     },
1330     process_sheet: function($sheet) {
1331         var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
1332         this.handle_common_properties($new_sheet, $sheet);
1333         var $dst = $new_sheet.find('.oe_form_sheet');
1334         $sheet.contents().appendTo($dst);
1335         $sheet.before($new_sheet).remove();
1336         this.process($new_sheet);
1337     },
1338     process_form: function($form) {
1339         if ($form.find('> sheet').length === 0) {
1340             $form.addClass('oe_form_nosheet');
1341         }
1342         var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
1343         this.handle_common_properties($new_form, $form);
1344         $form.contents().appendTo($new_form);
1345         if ($form[0] === this.$form[0]) {
1346             // If root element, replace it
1347             this.$form = $new_form;
1348         } else {
1349             $form.before($new_form).remove();
1350         }
1351         this.process($new_form);
1352     },
1353     /*
1354      * Used by direct <field> children of a <group> tag only
1355      * This method will add the implicit <label...> for every field
1356      * in the <group>
1357     */
1358     preprocess_field: function($field) {
1359         var self = this;
1360         var name = $field.attr('name'),
1361             field_colspan = parseInt($field.attr('colspan'), 10),
1362             field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
1363
1364         if ($field.attr('nolabel') === '1')
1365             return;
1366         $field.attr('nolabel', '1');
1367         var found = false;
1368         this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
1369             $(el).parents().each(function(unused, tag) {
1370                 var name = tag.tagName.toLowerCase();
1371                 if (name === "field" || name in self.tags_registry.map)
1372                     found = true;
1373             });
1374         });
1375         if (found)
1376             return;
1377
1378         var $label = $('<label/>').attr({
1379             'for' : name,
1380             "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
1381             "string": $field.attr('string'),
1382             "help": $field.attr('help'),
1383             "class": $field.attr('class'),
1384         });
1385         $label.insertBefore($field);
1386         if (field_colspan > 1) {
1387             $field.attr('colspan', field_colspan - 1);
1388         }
1389         return $label;
1390     },
1391     process_field: function($field) {
1392         if ($field.parent().is('group')) {
1393             // No implicit labels for normal fields, only for <group> direct children
1394             var $label = this.preprocess_field($field);
1395             if ($label) {
1396                 this.process($label);
1397             }
1398         }
1399         this.fields_to_init.push($field);
1400         return $field;
1401     },
1402     process_group: function($group) {
1403         var self = this;
1404         $group.children('field').each(function() {
1405             self.preprocess_field($(this));
1406         });
1407         var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
1408         var $table;
1409         if ($new_group.first().is('table.oe_form_group')) {
1410             $table = $new_group;
1411         } else if ($new_group.filter('table.oe_form_group').length) {
1412             $table = $new_group.filter('table.oe_form_group').first();
1413         } else {
1414             $table = $new_group.find('table.oe_form_group').first();
1415         }
1416
1417         var $tr, $td,
1418             cols = parseInt($group.attr('col') || 2, 10),
1419             row_cols = cols;
1420
1421         var children = [];
1422         $group.children().each(function(a,b,c) {
1423             var $child = $(this);
1424             var colspan = parseInt($child.attr('colspan') || 1, 10);
1425             var tagName = $child[0].tagName.toLowerCase();
1426             var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
1427             var newline = tagName === 'newline';
1428
1429             // Note FME: those classes are used in layout debug mode
1430             if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
1431                 $tr.addClass('oe_form_group_row_incomplete');
1432                 if (newline) {
1433                     $tr.addClass('oe_form_group_row_newline');
1434                 }
1435             }
1436             if (newline) {
1437                 $tr = null;
1438                 return;
1439             }
1440             if (!$tr || row_cols < colspan) {
1441                 $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
1442                 row_cols = cols;
1443             } else if (tagName==='group') {
1444                 // When <group> <group/><group/> </group>, we need a spacing between the two groups
1445                 $td.addClass('oe_group_right')
1446             }
1447             row_cols -= colspan;
1448
1449             // invisibility transfer
1450             var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
1451             var invisible = field_modifiers.invisible;
1452             self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
1453
1454             $tr.append($td.append($child));
1455             children.push($child[0]);
1456         });
1457         if (row_cols && $td) {
1458             $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
1459         }
1460         $group.before($new_group).remove();
1461
1462         $table.find('> tbody > tr').each(function() {
1463             var to_compute = [],
1464                 row_cols = cols,
1465                 total = 100;
1466             $(this).children().each(function() {
1467                 var $td = $(this),
1468                     $child = $td.children(':first');
1469                 if ($child.attr('cell-class')) {
1470                     $td.addClass($child.attr('cell-class'));
1471                 }
1472                 switch ($child[0].tagName.toLowerCase()) {
1473                     case 'separator':
1474                         break;
1475                     case 'label':
1476                         if ($child.attr('for')) {
1477                             $td.attr('width', '1%').addClass('oe_form_group_cell_label');
1478                             row_cols-= $td.attr('colspan') || 1;
1479                             total--;
1480                         }
1481                         break;
1482                     default:
1483                         var width = _.str.trim($child.attr('width') || ''),
1484                             iwidth = parseInt(width, 10);
1485                         if (iwidth) {
1486                             if (width.substr(-1) === '%') {
1487                                 total -= iwidth;
1488                                 width = iwidth + '%';
1489                             } else {
1490                                 // Absolute width
1491                                 $td.css('min-width', width + 'px');
1492                             }
1493                             $td.attr('width', width);
1494                             $child.removeAttr('width');
1495                             row_cols-= $td.attr('colspan') || 1;
1496                         } else {
1497                             to_compute.push($td);
1498                         }
1499
1500                 }
1501             });
1502             if (row_cols) {
1503                 var unit = Math.floor(total / row_cols);
1504                 if (!$(this).is('.oe_form_group_row_incomplete')) {
1505                     _.each(to_compute, function($td, i) {
1506                         var width = parseInt($td.attr('colspan'), 10) * unit;
1507                         $td.attr('width', width + '%');
1508                         total -= width;
1509                     });
1510                 }
1511             }
1512         });
1513         _.each(children, function(el) {
1514             self.process($(el));
1515         });
1516         this.handle_common_properties($new_group, $group);
1517         return $new_group;
1518     },
1519     process_notebook: function($notebook) {
1520         var self = this;
1521         var pages = [];
1522         $notebook.find('> page').each(function() {
1523             var $page = $(this);
1524             var page_attrs = $page.getAttributes();
1525             page_attrs.id = _.uniqueId('notebook_page_');
1526             var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
1527             $page.contents().appendTo($new_page);
1528             $page.before($new_page).remove();
1529             var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
1530             page_attrs.__page = $new_page;
1531             page_attrs.__ic = ic;
1532             pages.push(page_attrs);
1533
1534             $new_page.children().each(function() {
1535                 self.process($(this));
1536             });
1537         });
1538         var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
1539         $notebook.contents().appendTo($new_notebook);
1540         $notebook.before($new_notebook).remove();
1541         self.process($($new_notebook.children()[0]));
1542         //tabs and invisibility handling
1543         $new_notebook.tabs();
1544         _.each(pages, function(page, i) {
1545             if (! page.__ic)
1546                 return;
1547             page.__ic.on("change:effective_invisible", null, function() {
1548                 if (!page.__ic.get('effective_invisible') && page.autofocus) {
1549                     $new_notebook.tabs('select', i);
1550                     return;
1551                 }
1552                 var current = $new_notebook.tabs("option", "selected");
1553                 if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
1554                     return;
1555                 var first_visible = _.find(_.range(pages.length), function(i2) {
1556                     return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
1557                 });
1558                 if (first_visible !== undefined) {
1559                     $new_notebook.tabs('select', first_visible);
1560                 }
1561             });
1562         });
1563
1564         this.handle_common_properties($new_notebook, $notebook);
1565         return $new_notebook;
1566     },
1567     process_separator: function($separator) {
1568         var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
1569         $separator.before($new_separator).remove();
1570         this.handle_common_properties($new_separator, $separator);
1571         return $new_separator;
1572     },
1573     process_label: function($label) {
1574         var name = $label.attr("for"),
1575             field_orm = this.fvg.fields[name];
1576         var dict = {
1577             string: $label.attr('string') || (field_orm || {}).string || '',
1578             help: $label.attr('help') || (field_orm || {}).help || '',
1579             _for: name ? _.uniqueId('oe-field-input-') : undefined,
1580         };
1581         var align = parseFloat(dict.align);
1582         if (isNaN(align) || align === 1) {
1583             align = 'right';
1584         } else if (align === 0) {
1585             align = 'left';
1586         } else {
1587             align = 'center';
1588         }
1589         dict.align = align;
1590         var $new_label = this.render_element('FormRenderingLabel', dict);
1591         $label.before($new_label).remove();
1592         this.handle_common_properties($new_label, $label);
1593         if (name) {
1594             this.labels[name] = $new_label;
1595         }
1596         return $new_label;
1597     },
1598     handle_common_properties: function($new_element, $node) {
1599         var str_modifiers = $node.attr("modifiers") || "{}";
1600         var modifiers = JSON.parse(str_modifiers);
1601         var ic = null;
1602         if (modifiers.invisible !== undefined)
1603             ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
1604         $new_element.addClass($node.attr("class") || "");
1605         $new_element.attr('style', $node.attr('style'));
1606         return {invisibility_changer: ic,};
1607     },
1608 });
1609
1610 /**
1611     Welcome.
1612
1613     If you read this documentation, it probably means that you were asked to use a form view widget outside of
1614     a form view. Before going further, you must understand that those fields were never really created for
1615     that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
1616     you to hack the system with more style.
1617 */
1618 instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
1619     init: function(parent, eval_context) {
1620         this._super(parent);
1621         this.field_descs = {};
1622         this.eval_context = eval_context || {};
1623         this.set({
1624             display_invalid_fields: false,
1625             actual_mode: 'create',
1626         });
1627     },
1628     get_field_desc: function(field_name) {
1629         if (this.field_descs[field_name] === undefined) {
1630             this.field_descs[field_name] = {
1631                 string: field_name,
1632             };
1633         }
1634         return this.field_descs[field_name];
1635     },
1636     extend_field_desc: function(fields) {
1637         var self = this;
1638         _.each(fields, function(v, k) {
1639             _.extend(self.get_field_desc(k), v);
1640         });
1641     },
1642     get_field_value: function(field_name) {
1643         return false;
1644     },
1645     set_values: function(values) {
1646         // nothing
1647     },
1648     compute_domain: function(expression) {
1649         return instance.web.form.compute_domain(expression, {});
1650     },
1651     build_eval_context: function() {
1652         return new instance.web.CompoundContext(this.eval_context);
1653     },
1654 });
1655
1656 instance.web.form.compute_domain = function(expr, fields) {
1657     if (! (expr instanceof Array))
1658         return !! expr;
1659     var stack = [];
1660     for (var i = expr.length - 1; i >= 0; i--) {
1661         var ex = expr[i];
1662         if (ex.length == 1) {
1663             var top = stack.pop();
1664             switch (ex) {
1665                 case '|':
1666                     stack.push(stack.pop() || top);
1667                     continue;
1668                 case '&':
1669                     stack.push(stack.pop() && top);
1670                     continue;
1671                 case '!':
1672                     stack.push(!top);
1673                     continue;
1674                 default:
1675                     throw new Error(_.str.sprintf(
1676                         _t("Unknown operator %s in domain %s"),
1677                         ex, JSON.stringify(expr)));
1678             }
1679         }
1680
1681         var field = fields[ex[0]];
1682         if (!field) {
1683             throw new Error(_.str.sprintf(
1684                 _t("Unknown field %s in domain %s"),
1685                 ex[0], JSON.stringify(expr)));
1686         }
1687         var field_value = field.get_value ? field.get_value() : field.value;
1688         var op = ex[1];
1689         var val = ex[2];
1690
1691         switch (op.toLowerCase()) {
1692             case '=':
1693             case '==':
1694                 stack.push(_.isEqual(field_value, val));
1695                 break;
1696             case '!=':
1697             case '<>':
1698                 stack.push(!_.isEqual(field_value, val));
1699                 break;
1700             case '<':
1701                 stack.push(field_value < val);
1702                 break;
1703             case '>':
1704                 stack.push(field_value > val);
1705                 break;
1706             case '<=':
1707                 stack.push(field_value <= val);
1708                 break;
1709             case '>=':
1710                 stack.push(field_value >= val);
1711                 break;
1712             case 'in':
1713                 if (!_.isArray(val)) val = [val];
1714                 stack.push(_(val).contains(field_value));
1715                 break;
1716             case 'not in':
1717                 if (!_.isArray(val)) val = [val];
1718                 stack.push(!_(val).contains(field_value));
1719                 break;
1720             default:
1721                 console.warn(
1722                     _t("Unsupported operator %s in domain %s"),
1723                     op, JSON.stringify(expr));
1724         }
1725     }
1726     return _.all(stack, _.identity);
1727 };
1728
1729 instance.web.form.is_bin_size = function(v) {
1730     return /^\d+(\.\d*)? \w+$/.test(v);
1731 };
1732
1733 /**
1734  * Must be applied over an class already possessing the PropertiesMixin.
1735  *
1736  * Apply the result of the "invisible" domain to this.$el.
1737  */
1738 instance.web.form.InvisibilityChangerMixin = {
1739     init: function(field_manager, invisible_domain) {
1740         var self = this;
1741         this._ic_field_manager = field_manager;
1742         this._ic_invisible_modifier = invisible_domain;
1743         this._ic_field_manager.on("view_content_has_changed", this, function() {
1744             var result = self._ic_invisible_modifier === undefined ? false :
1745                 self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
1746             self.set({"invisible": result});
1747         });
1748         this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
1749         var check = function() {
1750             if (self.get("invisible") || self.get('force_invisible')) {
1751                 self.set({"effective_invisible": true});
1752             } else {
1753                 self.set({"effective_invisible": false});
1754             }
1755         };
1756         this.on('change:invisible', this, check);
1757         this.on('change:force_invisible', this, check);
1758         check.call(this);
1759     },
1760     start: function() {
1761         this.on("change:effective_invisible", this, this._check_visibility);
1762         this._check_visibility();
1763     },
1764     _check_visibility: function() {
1765         this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
1766     },
1767 };
1768
1769 instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
1770     init: function(parent, field_manager, invisible_domain, $el) {
1771         this.setParent(parent);
1772         instance.web.PropertiesMixin.init.call(this);
1773         instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
1774         this.$el = $el;
1775         this.start();
1776     },
1777 });
1778
1779 /**
1780     Base class for all fields, custom widgets and buttons to be displayed in the form view.
1781
1782     Properties:
1783         - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
1784         the values of the "readonly" property and the "mode" property on the field manager.
1785 */
1786 instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
1787     /**
1788      * @constructs instance.web.form.FormWidget
1789      * @extends instance.web.Widget
1790      *
1791      * @param field_manager
1792      * @param node
1793      */
1794     init: function(field_manager, node) {
1795         this._super(field_manager);
1796         this.field_manager = field_manager;
1797         if (this.field_manager instanceof instance.web.FormView)
1798             this.view = this.field_manager;
1799         this.node = node;
1800         this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
1801         instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
1802
1803         this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
1804
1805         this.set({
1806             required: false,
1807             readonly: false,
1808         });
1809         // some events to make the property "effective_readonly" sync automatically with "readonly" and
1810         // "mode" on field_manager
1811         var self = this;
1812         var test_effective_readonly = function() {
1813             self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
1814         };
1815         this.on("change:readonly", this, test_effective_readonly);
1816         this.field_manager.on("change:actual_mode", this, test_effective_readonly);
1817         test_effective_readonly.call(this);
1818     },
1819     renderElement: function() {
1820         this.process_modifiers();
1821         this._super();
1822         this.$el.addClass(this.node.attrs["class"] || "");
1823     },
1824     destroy: function() {
1825         $.fn.tipsy.clear();
1826         this._super.apply(this, arguments);
1827     },
1828     /**
1829      * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
1830      *
1831      * This method is an utility method that is meant to be called by child classes.
1832      *
1833      * @param {jQuery} $e jQuery object of elements to bind focus/blur on
1834      */
1835     setupFocus: function ($e) {
1836         var self = this;
1837         $e.on({
1838             focus: function () { self.trigger('focused'); },
1839             blur: function () { self.trigger('blurred'); }
1840         });
1841     },
1842     process_modifiers: function() {
1843         var to_set = {};
1844         for (var a in this.modifiers) {
1845             if (!this.modifiers.hasOwnProperty(a)) { continue; }
1846             if (!_.include(["invisible"], a)) {
1847                 var val = this.field_manager.compute_domain(this.modifiers[a]);
1848                 to_set[a] = val;
1849             }
1850         }
1851         this.set(to_set);
1852     },
1853     do_attach_tooltip: function(widget, trigger, options) {
1854         widget = widget || this;
1855         trigger = trigger || this.$el;
1856         options = _.extend({
1857                 delayIn: 500,
1858                 delayOut: 0,
1859                 fade: true,
1860                 title: function() {
1861                     var template = widget.template + '.tooltip';
1862                     if (!QWeb.has_template(template)) {
1863                         template = 'WidgetLabel.tooltip';
1864                     }
1865                     return QWeb.render(template, {
1866                         debug: instance.session.debug,
1867                         widget: widget
1868                 })},
1869                 gravity: $.fn.tipsy.autoBounds(50, 'nw'),
1870                 html: true,
1871                 opacity: 0.85,
1872                 trigger: 'hover'
1873             }, options || {});
1874         $(trigger).tipsy(options);
1875     },
1876     /**
1877      * Builds a new context usable for operations related to fields by merging
1878      * the fields'context with the action's context.
1879      */
1880     build_context: function() {
1881         // only use the model's context if there is not context on the node
1882         var v_context = this.node.attrs.context;
1883         if (! v_context) {
1884             v_context = (this.field || {}).context || {};
1885         }
1886
1887         if (v_context.__ref || true) { //TODO: remove true
1888             var fields_values = this.field_manager.build_eval_context();
1889             v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
1890         }
1891         return v_context;
1892     },
1893     build_domain: function() {
1894         var f_domain = this.field.domain || [];
1895         var n_domain = this.node.attrs.domain || null;
1896         // if there is a domain on the node, overrides the model's domain
1897         var final_domain = n_domain !== null ? n_domain : f_domain;
1898         if (!(final_domain instanceof Array) || true) { //TODO: remove true
1899             var fields_values = this.field_manager.build_eval_context();
1900             final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
1901         }
1902         return final_domain;
1903     }
1904 });
1905
1906 instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
1907     template: 'WidgetButton',
1908     init: function(field_manager, node) {
1909         node.attrs.type = node.attrs['data-button-type'];
1910         this._super(field_manager, node);
1911         this.force_disabled = false;
1912         this.string = (this.node.attrs.string || '').replace(/_/g, '');
1913         if (JSON.parse(this.node.attrs.default_focus || "0")) {
1914             // TODO fme: provide enter key binding to widgets
1915             this.view.default_focus_button = this;
1916         }
1917         if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
1918             this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
1919         }
1920     },
1921     start: function() {
1922         this._super.apply(this, arguments);
1923         this.view.on('view_content_has_changed', this, this.check_disable);
1924         this.check_disable();
1925         this.$el.click(this.on_click);
1926         if (this.node.attrs.help || instance.session.debug) {
1927             this.do_attach_tooltip();
1928         }
1929         this.setupFocus(this.$el);
1930     },
1931     on_click: function() {
1932         var self = this;
1933         this.force_disabled = true;
1934         this.check_disable();
1935         this.execute_action().always(function() {
1936             self.force_disabled = false;
1937             self.check_disable();
1938         });
1939     },
1940     execute_action: function() {
1941         var self = this;
1942         var exec_action = function() {
1943             if (self.node.attrs.confirm) {
1944                 var def = $.Deferred();
1945                 var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
1946                     title: _t('Confirm'),
1947                     modal: true,
1948                     buttons: [
1949                         {text: _t("Cancel"), click: function() {
1950                                 $(this).dialog("close");
1951                             }
1952                         },
1953                         {text: _t("Ok"), click: function() {
1954                                 var self2 = this;
1955                                 self.on_confirmed().always(function() {
1956                                     $(self2).dialog("close");
1957                                 });
1958                             }
1959                         }
1960                     ],
1961                     beforeClose: function() {
1962                         def.resolve();
1963                     },
1964                 });
1965                 return def.promise();
1966             } else {
1967                 return self.on_confirmed();
1968             }
1969         };
1970         if (!this.node.attrs.special) {
1971             return this.view.recursive_save().then(exec_action);
1972         } else {
1973             return exec_action();
1974         }
1975     },
1976     on_confirmed: function() {
1977         var self = this;
1978
1979         var context = this.build_context();
1980
1981         return this.view.do_execute_action(
1982             _.extend({}, this.node.attrs, {context: context}),
1983             this.view.dataset, this.view.datarecord.id, function (reason) {
1984                 if (!_.isObject(reason)) {
1985                     self.view.recursive_reload();
1986                 }
1987             });
1988     },
1989     check_disable: function() {
1990         var disabled = (this.force_disabled || !this.view.is_interactible_record());
1991         this.$el.prop('disabled', disabled);
1992         this.$el.css('color', disabled ? 'grey' : '');
1993     }
1994 });
1995
1996 /**
1997  * Interface to be implemented by fields.
1998  *
1999  * Events:
2000  *     - changed_value: triggered when the value of the field has changed. This can be due
2001  *      to a user interaction or a call to set_value().
2002  *
2003  */
2004 instance.web.form.FieldInterface = {
2005     /**
2006      * Constructor takes 2 arguments:
2007      * - field_manager: Implements FieldManagerMixin
2008      * - node: the "<field>" node in json form
2009      */
2010     init: function(field_manager, node) {},
2011     /**
2012      * Called by the form view to indicate the value of the field.
2013      *
2014      * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
2015      * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
2016      * before the widget is inserted into the DOM.
2017      *
2018      * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
2019      * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
2020      * also be able to handle any other format commonly used in the _defaults key on the models in the addons
2021      * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
2022      * no information is ever given to know which format is used.
2023      */
2024     set_value: function(value_) {},
2025     /**
2026      * Get the current value of the widget.
2027      *
2028      * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
2029      * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
2030      * For example if the field is marked as "required", a call to get_value() can return false.
2031      *
2032      * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
2033      * return a default value according to the type of field.
2034      *
2035      * This method is always assumed to perform synchronously, it can not return a promise.
2036      *
2037      * If there was no user interaction to modify the value of the field, it is always assumed that
2038      * get_value() return the same semantic value than the one passed in the last call to set_value(),
2039      * although the syntax can be different. This can be the case for type of fields that have a different
2040      * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
2041      */
2042     get_value: function() {},
2043     /**
2044      * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
2045      * view.
2046      */
2047     set_input_id: function(id) {},
2048     /**
2049      * Returns true if is_syntax_valid() returns true and the value is semantically
2050      * valid too according to the semantic restrictions applied to the field.
2051      */
2052     is_valid: function() {},
2053     /**
2054      * Returns true if the field holds a value which is syntactically correct, ignoring
2055      * the potential semantic restrictions applied to the field.
2056      */
2057     is_syntax_valid: function() {},
2058     /**
2059      * Must set the focus on the field. Return false if field is not focusable.
2060      */
2061     focus: function() {},
2062     /**
2063      * Called when the translate button is clicked.
2064      */
2065     on_translate: function() {},
2066     /**
2067         This method is called by the form view before reading on_change values and before saving. It tells
2068         the field to save its value before reading it using get_value(). Must return a promise.
2069     */
2070     commit_value: function() {},
2071 };
2072
2073 /**
2074  * Abstract class for classes implementing FieldInterface.
2075  *
2076  * Properties:
2077  *     - value: useful property to hold the value of the field. By default, set_value() and get_value()
2078  *     set and retrieve the value property. Changing the value property also triggers automatically
2079  *     a 'changed_value' event that inform the view to trigger on_changes.
2080  *
2081  */
2082 instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
2083     /**
2084      * @constructs instance.web.form.AbstractField
2085      * @extends instance.web.form.FormWidget
2086      *
2087      * @param field_manager
2088      * @param node
2089      */
2090     init: function(field_manager, node) {
2091         var self = this
2092         this._super(field_manager, node);
2093         this.name = this.node.attrs.name;
2094         this.field = this.field_manager.get_field_desc(this.name);
2095         this.widget = this.node.attrs.widget;
2096         this.string = this.node.attrs.string || this.field.string || this.name;
2097         this.options = instance.web.py_eval(this.node.attrs.options || '{}');
2098         this.set({'value': false});
2099
2100         this.on("change:value", this, function() {
2101             this.trigger('changed_value');
2102             this._check_css_flags();
2103         });
2104     },
2105     renderElement: function() {
2106         var self = this;
2107         this._super();
2108         if (this.field.translate && this.view) {
2109             this.$el.addClass('oe_form_field_translatable');
2110             this.$el.find('.oe_field_translate').click(this.on_translate);
2111         }
2112         this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
2113         if (instance.session.debug) {
2114             this.do_attach_tooltip(this, this.$label[0] || this.$el);
2115             this.$label.off('dblclick').on('dblclick', function() {
2116                 console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
2117                 window.w = self;
2118                 console.log("window.w =", window.w);
2119             });
2120         }
2121         if (!this.disable_utility_classes) {
2122             this.off("change:required", this, this._set_required);
2123             this.on("change:required", this, this._set_required);
2124             this._set_required();
2125         }
2126         this._check_visibility();
2127         this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
2128         this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
2129         this._check_css_flags();
2130     },
2131     start: function() {
2132         var tmp = this._super();
2133         this.on("change:value", this, function() {
2134             if (! this.no_rerender)
2135                 this.render_value();
2136         });
2137         this.render_value();
2138     },
2139     /**
2140      * Private. Do not use.
2141      */
2142     _set_required: function() {
2143         this.$el.toggleClass('oe_form_required', this.get("required"));
2144     },
2145     set_value: function(value_) {
2146         this.set({'value': value_});
2147     },
2148     get_value: function() {
2149         return this.get('value');
2150     },
2151     /**
2152         Utility method that all implementations should use to change the
2153         value without triggering a re-rendering.
2154     */
2155     internal_set_value: function(value_) {
2156         var tmp = this.no_rerender;
2157         this.no_rerender = true;
2158         this.set({'value': value_});
2159         this.no_rerender = tmp;
2160     },
2161     /**
2162         This method is called each time the value is modified.
2163     */
2164     render_value: function() {},
2165     is_valid: function() {
2166         return this.is_syntax_valid() && !(this.get('required') && this.is_false());
2167     },
2168     is_syntax_valid: function() {
2169         return true;
2170     },
2171     /**
2172      * Method useful to implement to ease validity testing. Must return true if the current
2173      * value is similar to false in OpenERP.
2174      */
2175     is_false: function() {
2176         return this.get('value') === false;
2177     },
2178     _check_css_flags: function() {
2179         if (this.field.translate) {
2180             this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
2181         }
2182         if (!this.disable_utility_classes) {
2183             if (this.field_manager.get('display_invalid_fields')) {
2184                 this.$el.toggleClass('oe_form_invalid', !this.is_valid());
2185             }
2186         }
2187     },
2188     focus: function() {
2189         return false;
2190     },
2191     set_input_id: function(id) {
2192         this.id_for_label = id;
2193     },
2194     on_translate: function() {
2195         var self = this;
2196         var trans = new instance.web.DataSet(this, 'ir.translation');
2197         return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
2198             self.do_action(r);
2199         });
2200     },
2201
2202     set_dimensions: function (height, width) {
2203         this.$el.css({
2204             width: width,
2205             minHeight: height
2206         });
2207     },
2208     commit_value: function() {
2209         return $.when();
2210     },
2211 });
2212
2213 /**
2214  * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
2215  * switch.
2216  */
2217 instance.web.form.ReinitializeWidgetMixin =  {
2218     /**
2219      * Default implementation of, you should not override it, use initialize_field() instead.
2220      */
2221     start: function() {
2222         this.initialize_field();
2223         this._super();
2224     },
2225     initialize_field: function() {
2226         this.on("change:effective_readonly", this, this.reinitialize);
2227         this.initialize_content();
2228     },
2229     reinitialize: function() {
2230         this.destroy_content();
2231         this.renderElement();
2232         this.initialize_content();
2233     },
2234     /**
2235      * Called to destroy anything that could have been created previously, called before a
2236      * re-initialization.
2237      */
2238     destroy_content: function() {},
2239     /**
2240      * Called to initialize the content.
2241      */
2242     initialize_content: function() {},
2243 };
2244
2245 /**
2246  * A mixin to apply on any field that has to completely re-render when its readonly state
2247  * switch.
2248  */
2249 instance.web.form.ReinitializeFieldMixin =  _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
2250     reinitialize: function() {
2251         instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
2252         var res = this.render_value();
2253         if (this.view && this.view.render_value_defs){
2254             this.view.render_value_defs.push(res);
2255         }
2256     },
2257 });
2258
2259 /**
2260     Some hack to make placeholders work in ie9.
2261 */
2262 if ($.browser.msie && $.browser.version === "9.0") {
2263     document.addEventListener("DOMNodeInserted",function(event){
2264         var nodename =  event.target.nodeName.toLowerCase();
2265         if ( nodename === "input" || nodename == "textarea" ) {
2266             $(event.target).placeholder();
2267         }
2268     });
2269 }
2270
2271 instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2272     template: 'FieldChar',
2273     widget_class: 'oe_form_field_char',
2274     events: {
2275         'change input': 'store_dom_value',
2276     },
2277     init: function (field_manager, node) {
2278         this._super(field_manager, node);
2279         this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2280     },
2281     initialize_content: function() {
2282         this.setupFocus(this.$('input'));
2283     },
2284     store_dom_value: function () {
2285         if (!this.get('effective_readonly')
2286                 && this.$('input').length
2287                 && this.is_syntax_valid()) {
2288             this.internal_set_value(
2289                 this.parse_value(
2290                     this.$('input').val()));
2291         }
2292     },
2293     commit_value: function () {
2294         this.store_dom_value();
2295         return this._super();
2296     },
2297     render_value: function() {
2298         var show_value = this.format_value(this.get('value'), '');
2299         if (!this.get("effective_readonly")) {
2300             this.$el.find('input').val(show_value);
2301         } else {
2302             if (this.password) {
2303                 show_value = new Array(show_value.length + 1).join('*');
2304             }
2305             this.$(".oe_form_char_content").text(show_value);
2306         }
2307     },
2308     is_syntax_valid: function() {
2309         if (!this.get("effective_readonly") && this.$("input").size() > 0) {
2310             try {
2311                 this.parse_value(this.$('input').val(), '');
2312                 return true;
2313             } catch(e) {
2314                 return false;
2315             }
2316         }
2317         return true;
2318     },
2319     parse_value: function(val, def) {
2320         return instance.web.parse_value(val, this, def);
2321     },
2322     format_value: function(val, def) {
2323         return instance.web.format_value(val, this, def);
2324     },
2325     is_false: function() {
2326         return this.get('value') === '' || this._super();
2327     },
2328     focus: function() {
2329         var input = this.$('input:first')[0];
2330         return input ? input.focus() : false;
2331     },
2332     set_dimensions: function (height, width) {
2333         this._super(height, width);
2334         this.$('input').css({
2335             height: height,
2336             width: width
2337         });
2338     }
2339 });
2340
2341 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2342     process_modifiers: function () {
2343         this._super();
2344         this.set({ readonly: true });
2345     },
2346 });
2347
2348 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2349     template: 'FieldEmail',
2350     initialize_content: function() {
2351         this._super();
2352         var $button = this.$el.find('button');
2353         $button.click(this.on_button_clicked);
2354         this.setupFocus($button);
2355     },
2356     render_value: function() {
2357         if (!this.get("effective_readonly")) {
2358             this._super();
2359         } else {
2360             this.$el.find('a')
2361                     .attr('href', 'mailto:' + this.get('value'))
2362                     .text(this.get('value') || '');
2363         }
2364     },
2365     on_button_clicked: function() {
2366         if (!this.get('value') || !this.is_syntax_valid()) {
2367             this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2368         } else {
2369             location.href = 'mailto:' + this.get('value');
2370         }
2371     }
2372 });
2373
2374 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2375     template: 'FieldUrl',
2376     initialize_content: function() {
2377         this._super();
2378         var $button = this.$el.find('button');
2379         $button.click(this.on_button_clicked);
2380         this.setupFocus($button);
2381     },
2382     render_value: function() {
2383         if (!this.get("effective_readonly")) {
2384             this._super();
2385         } else {
2386             var tmp = this.get('value');
2387             var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2388             if (!s) {
2389                 tmp = "http://" + this.get('value');
2390             }
2391             var text = this.get('value') ? this.node.attrs.text || tmp : '';
2392             this.$el.find('a').attr('href', tmp).text(text);
2393         }
2394     },
2395     on_button_clicked: function() {
2396         if (!this.get('value')) {
2397             this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2398         } else {
2399             var url = $.trim(this.get('value'));
2400             if(/^www\./i.test(url))
2401                 url = 'http://'+url;
2402             window.open(url);
2403         }
2404     }
2405 });
2406
2407 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2408     is_field_number: true,
2409     widget_class: 'oe_form_field_float',
2410     init: function (field_manager, node) {
2411         this._super(field_manager, node);
2412         this.internal_set_value(0);
2413         if (this.node.attrs.digits) {
2414             this.digits = this.node.attrs.digits;
2415         } else {
2416             this.digits = this.field.digits;
2417         }
2418     },
2419     set_value: function(value_) {
2420         if (value_ === false || value_ === undefined) {
2421             // As in GTK client, floats default to 0
2422             value_ = 0;
2423         }
2424         this._super.apply(this, [value_]);
2425     },
2426     focus: function () {
2427         var $input = this.$('input:first');
2428         return $input.length ? $input.select() : false;
2429     }
2430 });
2431
2432 instance.web.DateTimeWidget = instance.web.Widget.extend({
2433     template: "web.datepicker",
2434     jqueryui_object: 'datetimepicker',
2435     type_of_date: "datetime",
2436     events: {
2437         'change .oe_datepicker_master': 'change_datetime',
2438         'keypress .oe_datepicker_master': 'change_datetime',
2439     },
2440     init: function(parent) {
2441         this._super(parent);
2442         this.name = parent.name;
2443     },
2444     start: function() {
2445         var self = this;
2446         this.$input = this.$el.find('input.oe_datepicker_master');
2447         this.$input_picker = this.$el.find('input.oe_datepicker_container');
2448
2449         $.datepicker.setDefaults({
2450             clearText: _t('Clear'),
2451             clearStatus: _t('Erase the current date'),
2452             closeText: _t('Done'),
2453             closeStatus: _t('Close without change'),
2454             prevText: _t('<Prev'),
2455             prevStatus: _t('Show the previous month'),
2456             nextText: _t('Next>'),
2457             nextStatus: _t('Show the next month'),
2458             currentText: _t('Today'),
2459             currentStatus: _t('Show the current month'),
2460             monthNames: Date.CultureInfo.monthNames,
2461             monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
2462             monthStatus: _t('Show a different month'),
2463             yearStatus: _t('Show a different year'),
2464             weekHeader: _t('Wk'),
2465             weekStatus: _t('Week of the year'),
2466             dayNames: Date.CultureInfo.dayNames,
2467             dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
2468             dayNamesMin: Date.CultureInfo.shortestDayNames,
2469             dayStatus: _t('Set DD as first week day'),
2470             dateStatus: _t('Select D, M d'),
2471             firstDay: Date.CultureInfo.firstDayOfWeek,
2472             initStatus: _t('Select a date'),
2473             isRTL: false
2474         });
2475         $.timepicker.setDefaults({
2476             timeOnlyTitle: _t('Choose Time'),
2477             timeText: _t('Time'),
2478             hourText: _t('Hour'),
2479             minuteText: _t('Minute'),
2480             secondText: _t('Second'),
2481             currentText: _t('Now'),
2482             closeText: _t('Done')
2483         });
2484
2485         this.picker({
2486             onClose: this.on_picker_select,
2487             onSelect: this.on_picker_select,
2488             changeMonth: true,
2489             changeYear: true,
2490             showWeek: true,
2491             showButtonPanel: true,
2492             firstDay: Date.CultureInfo.firstDayOfWeek
2493         });
2494         // Some clicks in the datepicker dialog are not stopped by the
2495         // datepicker and "bubble through", unexpectedly triggering the bus's
2496         // click event. Prevent that.
2497         this.picker('widget').click(function (e) { e.stopPropagation(); });
2498
2499         this.$el.find('img.oe_datepicker_trigger').click(function() {
2500             if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
2501                 self.$input.focus();
2502                 return;
2503             }
2504             self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
2505             self.$input_picker.show();
2506             self.picker('show');
2507             self.$input_picker.hide();
2508         });
2509         this.set_readonly(false);
2510         this.set({'value': false});
2511     },
2512     picker: function() {
2513         return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
2514     },
2515     on_picker_select: function(text, instance_) {
2516         var date = this.picker('getDate');
2517         this.$input
2518             .val(date ? this.format_client(date) : '')
2519             .change()
2520             .focus();
2521     },
2522     set_value: function(value_) {
2523         this.set({'value': value_});
2524         this.$input.val(value_ ? this.format_client(value_) : '');
2525     },
2526     get_value: function() {
2527         return this.get('value');
2528     },
2529     set_value_from_ui_: function() {
2530         var value_ = this.$input.val() || false;
2531         this.set({'value': this.parse_client(value_)});
2532     },
2533     set_readonly: function(readonly) {
2534         this.readonly = readonly;
2535         this.$input.prop('readonly', this.readonly);
2536         this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
2537     },
2538     is_valid_: function() {
2539         var value_ = this.$input.val();
2540         if (value_ === "") {
2541             return true;
2542         } else {
2543             try {
2544                 this.parse_client(value_);
2545                 return true;
2546             } catch(e) {
2547                 return false;
2548             }
2549         }
2550     },
2551     parse_client: function(v) {
2552         return instance.web.parse_value(v, {"widget": this.type_of_date});
2553     },
2554     format_client: function(v) {
2555         return instance.web.format_value(v, {"widget": this.type_of_date});
2556     },
2557     change_datetime: function(e) {
2558         if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2559             this.set_value_from_ui_();
2560             this.trigger("datetime_changed");
2561         }
2562     },
2563     commit_value: function () {
2564         this.change_datetime();
2565     },
2566 });
2567
2568 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2569     jqueryui_object: 'datepicker',
2570     type_of_date: "date"
2571 });
2572
2573 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2574     template: "FieldDatetime",
2575     build_widget: function() {
2576         return new instance.web.DateTimeWidget(this);
2577     },
2578     destroy_content: function() {
2579         if (this.datewidget) {
2580             this.datewidget.destroy();
2581             this.datewidget = undefined;
2582         }
2583     },
2584     initialize_content: function() {
2585         if (!this.get("effective_readonly")) {
2586             this.datewidget = this.build_widget();
2587             this.datewidget.on('datetime_changed', this, _.bind(function() {
2588                 this.internal_set_value(this.datewidget.get_value());
2589             }, this));
2590             this.datewidget.appendTo(this.$el);
2591             this.setupFocus(this.datewidget.$input);
2592         }
2593     },
2594     render_value: function() {
2595         if (!this.get("effective_readonly")) {
2596             this.datewidget.set_value(this.get('value'));
2597         } else {
2598             this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2599         }
2600     },
2601     is_syntax_valid: function() {
2602         if (!this.get("effective_readonly") && this.datewidget) {
2603             return this.datewidget.is_valid_();
2604         }
2605         return true;
2606     },
2607     is_false: function() {
2608         return this.get('value') === '' || this._super();
2609     },
2610     focus: function() {
2611         var input = this.datewidget && this.datewidget.$input[0];
2612         return input ? input.focus() : false;
2613     },
2614     set_dimensions: function (height, width) {
2615         this._super(height, width);
2616         if (!this.get("effective_readonly")) {
2617             this.datewidget.$input.css('height', height);
2618         }
2619     }
2620 });
2621
2622 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2623     template: "FieldDate",
2624     build_widget: function() {
2625         return new instance.web.DateWidget(this);
2626     }
2627 });
2628
2629 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2630     template: 'FieldText',
2631     events: {
2632         'keyup': function (e) {
2633             if (e.which === $.ui.keyCode.ENTER) {
2634                 e.stopPropagation();
2635             }
2636         },
2637         'keypress': function (e) {
2638             if (e.which === $.ui.keyCode.ENTER) {
2639                 e.stopPropagation();
2640             }
2641         },
2642         'change textarea': 'store_dom_value',
2643     },
2644     initialize_content: function() {
2645         var self = this;
2646         if (! this.get("effective_readonly")) {
2647             this.$textarea = this.$el.find('textarea');
2648             this.auto_sized = false;
2649             this.default_height = this.$textarea.css('height');
2650             if (this.get("effective_readonly")) {
2651                 this.$textarea.attr('disabled', 'disabled');
2652             }
2653             this.setupFocus(this.$textarea);
2654         } else {
2655             this.$textarea = undefined;
2656         }
2657     },
2658     commit_value: function () {
2659         if (! this.get("effective_readonly") && this.$textarea) {
2660             this.store_dom_value();
2661         }
2662         return this._super();
2663     },
2664     store_dom_value: function () {
2665         this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2666     },
2667     render_value: function() {
2668         if (! this.get("effective_readonly")) {
2669             var show_value = instance.web.format_value(this.get('value'), this, '');
2670             if (show_value === '') {
2671                 this.$textarea.css('height', parseInt(this.default_height)+"px");
2672             }
2673             this.$textarea.val(show_value);
2674             if (! this.auto_sized) {
2675                 this.auto_sized = true;
2676                 this.$textarea.autosize();
2677             } else {
2678                 this.$textarea.trigger("autosize");
2679             }
2680         } else {
2681             var txt = this.get("value") || '';
2682             this.$(".oe_form_text_content").text(txt);
2683         }
2684     },
2685     is_syntax_valid: function() {
2686         if (!this.get("effective_readonly") && this.$textarea) {
2687             try {
2688                 instance.web.parse_value(this.$textarea.val(), this, '');
2689                 return true;
2690             } catch(e) {
2691                 return false;
2692             }
2693         }
2694         return true;
2695     },
2696     is_false: function() {
2697         return this.get('value') === '' || this._super();
2698     },
2699     focus: function($el) {
2700         var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2701         return input ? input.focus() : false;
2702     },
2703     set_dimensions: function (height, width) {
2704         this._super(height, width);
2705         if (!this.get("effective_readonly") && this.$textarea) {
2706             this.$textarea.css({
2707                 width: width,
2708                 minHeight: height
2709             });
2710         }
2711     },
2712 });
2713
2714 /**
2715  * FieldTextHtml Widget
2716  * Intended for FieldText widgets meant to display HTML content. This
2717  * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2718  * To find more information about CLEditor configutation: go to
2719  * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2720  */
2721 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2722     template: 'FieldTextHtml',
2723     init: function() {
2724         this._super.apply(this, arguments);
2725     },
2726     initialize_content: function() {
2727         var self = this;
2728         if (! this.get("effective_readonly")) {
2729             self._updating_editor = false;
2730             this.$textarea = this.$el.find('textarea');
2731             var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2732             var height = ((this.node.attrs || {}).editor_height || 250);
2733             this.$textarea.cleditor({
2734                 width:      width, // width not including margins, borders or padding
2735                 height:     height, // height not including margins, borders or padding
2736                 controls:   // controls to add to the toolbar
2737                             "bold italic underline strikethrough " +
2738                             "| removeformat | bullets numbering | outdent " +
2739                             "indent | link unlink | source",
2740                 bodyStyle:  // style to assign to document body contained within the editor
2741                             "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2742             });
2743             this.$cleditor = this.$textarea.cleditor()[0];
2744             this.$cleditor.change(function() {
2745                 if (! self._updating_editor) {
2746                     self.$cleditor.updateTextArea();
2747                     self.internal_set_value(self.$textarea.val());
2748                 }
2749             });
2750             if (this.field.translate) {
2751                 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"/>')
2752                     .click(this.on_translate);
2753                 this.$cleditor.$toolbar.append($img);
2754             }
2755         }
2756     },
2757     render_value: function() {
2758         if (! this.get("effective_readonly")) {
2759             this.$textarea.val(this.get('value') || '');
2760             this._updating_editor = true;
2761             this.$cleditor.updateFrame();
2762             this._updating_editor = false;
2763         } else {
2764             this.$el.html(this.get('value'));
2765         }
2766     },
2767 });
2768
2769 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2770     template: 'FieldBoolean',
2771     start: function() {
2772         var self = this;
2773         this.$checkbox = $("input", this.$el);
2774         this.setupFocus(this.$checkbox);
2775         this.$el.click(_.bind(function() {
2776             this.internal_set_value(this.$checkbox.is(':checked'));
2777         }, this));
2778         var check_readonly = function() {
2779             self.$checkbox.prop('disabled', self.get("effective_readonly"));
2780         };
2781         this.on("change:effective_readonly", this, check_readonly);
2782         check_readonly.call(this);
2783         this._super.apply(this, arguments);
2784     },
2785     render_value: function() {
2786         this.$checkbox[0].checked = this.get('value');
2787     },
2788     focus: function() {
2789         var input = this.$checkbox && this.$checkbox[0];
2790         return input ? input.focus() : false;
2791     }
2792 });
2793
2794 /**
2795     The progressbar field expect a float from 0 to 100.
2796 */
2797 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2798     template: 'FieldProgressBar',
2799     render_value: function() {
2800         this.$el.progressbar({
2801             value: this.get('value') || 0,
2802             disabled: this.get("effective_readonly")
2803         });
2804         var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2805         this.$('span').html(formatted_value + '%');
2806     }
2807 });
2808
2809
2810 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2811     template: 'FieldSelection',
2812     events: {
2813         'change select': 'store_dom_value',
2814     },
2815     init: function(field_manager, node) {
2816         var self = this;
2817         this._super(field_manager, node);
2818         this.values = _(this.field.selection).chain()
2819             .reject(function (v) { return v[0] === false && v[1] === ''; })
2820             .unshift([false, ''])
2821             .value();
2822     },
2823     initialize_content: function() {
2824         // Flag indicating whether we're in an event chain containing a change
2825         // event on the select, in order to know what to do on keyup[RETURN]:
2826         // * If the user presses [RETURN] as part of changing the value of a
2827         //   selection, we should just let the value change and not let the
2828         //   event broadcast further (e.g. to validating the current state of
2829         //   the form in editable list view, which would lead to saving the
2830         //   current row or switching to the next one)
2831         // * If the user presses [RETURN] with a select closed (side-effect:
2832         //   also if the user opened the select and pressed [RETURN] without
2833         //   changing the selected value), takes the action as validating the
2834         //   row
2835         var ischanging = false;
2836         var $select = this.$el.find('select')
2837             .change(function () { ischanging = true; })
2838             .click(function () { ischanging = false; })
2839             .keyup(function (e) {
2840                 if (e.which !== 13 || !ischanging) { return; }
2841                 e.stopPropagation();
2842                 ischanging = false;
2843             });
2844         this.setupFocus($select);
2845     },
2846     commit_value: function () {
2847         this.store_dom_value();
2848         return this._super();
2849     },
2850     store_dom_value: function () {
2851         if (!this.get('effective_readonly') && this.$('select').length) {
2852             this.internal_set_value(
2853                 this.values[this.$('select')[0].selectedIndex][0]);
2854         }
2855     },
2856     set_value: function(value_) {
2857         value_ = value_ === null ? false : value_;
2858         value_ = value_ instanceof Array ? value_[0] : value_;
2859         this._super(value_);
2860     },
2861     render_value: function() {
2862         if (!this.get("effective_readonly")) {
2863             var index = 0;
2864             for (var i = 0, ii = this.values.length; i < ii; i++) {
2865                 if (this.values[i][0] === this.get('value')) index = i;
2866             }
2867             this.$el.find('select')[0].selectedIndex = index;
2868         } else {
2869             var self = this;
2870             var option = _(this.values)
2871                 .detect(function (record) { return record[0] === self.get('value'); });
2872             this.$el.text(option ? option[1] : this.values[0][1]);
2873         }
2874     },
2875     focus: function() {
2876         var input = this.$('select:first')[0];
2877         return input ? input.focus() : false;
2878     },
2879     set_dimensions: function (height, width) {
2880         this._super(height, width);
2881         this.$('select').css({
2882             height: height,
2883             width: width
2884         });
2885     }
2886 });
2887
2888 // jquery autocomplete tweak to allow html and classnames
2889 (function() {
2890     var proto = $.ui.autocomplete.prototype,
2891         initSource = proto._initSource;
2892
2893     function filter( array, term ) {
2894         var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2895         return $.grep( array, function(value_) {
2896             return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2897         });
2898     }
2899
2900     $.extend( proto, {
2901         _initSource: function() {
2902             if ( this.options.html && $.isArray(this.options.source) ) {
2903                 this.source = function( request, response ) {
2904                     response( filter( this.options.source, request.term ) );
2905                 };
2906             } else {
2907                 initSource.call( this );
2908             }
2909         },
2910
2911         _renderItem: function( ul, item) {
2912             return $( "<li></li>" )
2913                 .data( "item.autocomplete", item )
2914                 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2915                 .appendTo( ul )
2916                 .addClass(item.classname);
2917         }
2918     });
2919 })();
2920
2921 /**
2922  * A mixin containing some useful methods to handle completion inputs.
2923  */
2924 instance.web.form.CompletionFieldMixin = {
2925     init: function() {
2926         this.limit = 7;
2927         this.orderer = new instance.web.DropMisordered();
2928     },
2929     /**
2930      * Call this method to search using a string.
2931      */
2932     get_search_result: function(search_val) {
2933         var self = this;
2934
2935         var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
2936         var blacklist = this.get_search_blacklist();
2937         this.last_query = search_val;
2938
2939         return this.orderer.add(dataset.name_search(
2940                 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
2941                 'ilike', this.limit + 1, self.build_context())).then(function(data) {
2942             self.last_search = data;
2943             // possible selections for the m2o
2944             var values = _.map(data, function(x) {
2945                 x[1] = x[1].split("\n")[0];
2946                 return {
2947                     label: _.str.escapeHTML(x[1]),
2948                     value: x[1],
2949                     name: x[1],
2950                     id: x[0],
2951                 };
2952             });
2953
2954             // search more... if more results that max
2955             if (values.length > self.limit) {
2956                 values = values.slice(0, self.limit);
2957                 values.push({
2958                     label: _t("Search More..."),
2959                     action: function() {
2960                         dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
2961                             self._search_create_popup("search", data);
2962                         });
2963                     },
2964                     classname: 'oe_m2o_dropdown_option'
2965                 });
2966             }
2967             // quick create
2968             var raw_result = _(data.result).map(function(x) {return x[1];});
2969             if (search_val.length > 0 && !_.include(raw_result, search_val)) {
2970                 values.push({
2971                     label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
2972                         $('<span />').text(search_val).html()),
2973                     action: function() {
2974                         self._quick_create(search_val);
2975                     },
2976                     classname: 'oe_m2o_dropdown_option'
2977                 });
2978             }
2979             // create...
2980             values.push({
2981                 label: _t("Create and Edit..."),
2982                 action: function() {
2983                     self._search_create_popup("form", undefined, self._create_context(search_val));
2984                 },
2985                 classname: 'oe_m2o_dropdown_option'
2986             });
2987
2988             return values;
2989         });
2990     },
2991     get_search_blacklist: function() {
2992         return [];
2993     },
2994     _quick_create: function(name) {
2995         var self = this;
2996         var slow_create = function () {
2997             self._search_create_popup("form", undefined, self._create_context(name));
2998         };
2999         if (self.options.quick_create === undefined || self.options.quick_create) {
3000             new instance.web.DataSet(this, this.field.relation, self.build_context())
3001                 .name_create(name).done(function(data) {
3002                     if (!self.get('effective_readonly'))
3003                         self.add_id(data[0]);
3004                 }).fail(function(error, event) {
3005                     event.preventDefault();
3006                     slow_create();
3007                 });
3008         } else
3009             slow_create();
3010     },
3011     // all search/create popup handling
3012     _search_create_popup: function(view, ids, context) {
3013         var self = this;
3014         var pop = new instance.web.form.SelectCreatePopup(this);
3015         pop.select_element(
3016             self.field.relation,
3017             {
3018                 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3019                 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
3020                 initial_view: view,
3021                 disable_multiple_selection: true
3022             },
3023             self.build_domain(),
3024             new instance.web.CompoundContext(self.build_context(), context || {})
3025         );
3026         pop.on("elements_selected", self, function(element_ids) {
3027             self.add_id(element_ids[0]);
3028             self.focus();
3029         });
3030     },
3031     /**
3032      * To implement.
3033      */
3034     add_id: function(id) {},
3035     _create_context: function(name) {
3036         var tmp = {};
3037         var field = (this.options || {}).create_name_field;
3038         if (field === undefined)
3039             field = "name";
3040         if (field !== false && name && (this.options || {}).quick_create !== false)
3041             tmp["default_" + field] = name;
3042         return tmp;
3043     },
3044 };
3045
3046 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3047     template: "M2ODialog",
3048     init: function(parent) {
3049         this._super(parent, {
3050             title: _.str.sprintf(_t("Add %s"), parent.string),
3051             width: 312,
3052         });
3053     },
3054     start: function() {
3055         var self = this;
3056         this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3057         this.$("input").val(this.getParent().last_query);
3058         this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3059             self.getParent()._quick_create(self.$("input").val());
3060             self.destroy();
3061         });
3062         this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3063             self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3064             self.destroy();
3065         });
3066         this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3067             self.destroy();
3068         });
3069     },
3070 });
3071
3072 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3073     template: "FieldMany2One",
3074     events: {
3075         'keydown input': function (e) {
3076             switch (e.which) {
3077             case $.ui.keyCode.UP:
3078             case $.ui.keyCode.DOWN:
3079                 e.stopPropagation();
3080             }
3081         },
3082     },
3083     init: function(field_manager, node) {
3084         this._super(field_manager, node);
3085         instance.web.form.CompletionFieldMixin.init.call(this);
3086         this.set({'value': false});
3087         this.display_value = {};
3088         this.last_search = [];
3089         this.floating = false;
3090         this.current_display = null;
3091         this.is_started = false;
3092         this.ignore_focusout = false;
3093     },
3094     reinit_value: function(val) {
3095         this.internal_set_value(val);
3096         this.floating = false;
3097         if (this.is_started)
3098             this.render_value();
3099     },
3100     initialize_field: function() {
3101         this.is_started = true;
3102         instance.web.bus.on('click', this, function() {
3103             if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3104                 this.$input.autocomplete("close");
3105             }
3106         });
3107         instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3108     },
3109     initialize_content: function() {
3110         if (!this.get("effective_readonly"))
3111             this.render_editable();
3112     },
3113     destroy_content: function () {
3114         if (this.$drop_down) {
3115             this.$drop_down.off('click');
3116             delete this.$drop_down;
3117         }
3118         if (this.$input) {
3119             this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3120             this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3121                             'focus focusout change keydown');
3122             delete this.$input;
3123         }
3124         if (this.$follow_button) {
3125             this.$follow_button.off('blur focus click');
3126             delete this.$follow_button;
3127         }
3128     },
3129     destroy: function () {
3130         this.destroy_content();
3131         return this._super();
3132     },
3133     init_error_displayer: function() {
3134         // nothing
3135     },
3136     hide_error_displayer: function() {
3137         // doesn't work
3138     },
3139     show_error_displayer: function() {
3140         new instance.web.form.M2ODialog(this).open();
3141     },
3142     render_editable: function() {
3143         var self = this;
3144         this.$input = this.$el.find("input");
3145
3146         this.init_error_displayer();
3147
3148         self.$input.on('focus', function() {
3149             self.hide_error_displayer();
3150         });
3151
3152         this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3153         this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3154
3155         this.$follow_button.click(function(ev) {
3156             ev.preventDefault();
3157             if (!self.get('value')) {
3158                 self.focus();
3159                 return;
3160             }
3161             var pop = new instance.web.form.FormOpenPopup(self);
3162             pop.show_element(
3163                 self.field.relation,
3164                 self.get("value"),
3165                 self.build_context(),
3166                 {
3167                     title: _t("Open: ") + self.string
3168                 }
3169             );
3170             pop.on('write_completed', self, function(){
3171                 self.display_value = {};
3172                 self.render_value();
3173                 self.focus();
3174                 self.trigger('changed_value');
3175             });
3176         });
3177
3178         // some behavior for input
3179         var input_changed = function() {
3180             if (self.current_display !== self.$input.val()) {
3181                 self.current_display = self.$input.val();
3182                 if (self.$input.val() === "") {
3183                     self.internal_set_value(false);
3184                     self.floating = false;
3185                 } else {
3186                     self.floating = true;
3187                 }
3188             }
3189         };
3190         this.$input.keydown(input_changed);
3191         this.$input.change(input_changed);
3192         this.$drop_down.click(function() {
3193             if (self.$input.autocomplete("widget").is(":visible")) {
3194                 self.$input.autocomplete("close");
3195                 self.$input.focus();
3196             } else {
3197                 if (self.get("value") && ! self.floating) {
3198                     self.$input.autocomplete("search", "");
3199                 } else {
3200                     self.$input.autocomplete("search");
3201                 }
3202             }
3203         });
3204
3205         // Autocomplete close on dialog content scroll
3206         var close_autocomplete = _.debounce(function() {
3207             if (self.$input.autocomplete("widget").is(":visible")) {
3208                 self.$input.autocomplete("close");
3209             }
3210         }, 50);
3211         this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3212
3213         self.ed_def = $.Deferred();
3214         self.uned_def = $.Deferred();
3215         var ed_delay = 200;
3216         var ed_duration = 15000;
3217         var anyoneLoosesFocus = function (e) {
3218             if (self.ignore_focusout) { return; }
3219             var used = false;
3220             if (self.floating) {
3221                 if (self.last_search.length > 0) {
3222                     if (self.last_search[0][0] != self.get("value")) {
3223                         self.display_value = {};
3224                         self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3225                         self.reinit_value(self.last_search[0][0]);
3226                     } else {
3227                         used = true;
3228                         self.render_value();
3229                     }
3230                 } else {
3231                     used = true;
3232                     self.reinit_value(false);
3233                 }
3234                 self.floating = false;
3235             }
3236             if (used && self.get("value") === false && ! self.no_ed) {
3237                 self.ed_def.reject();
3238                 self.uned_def.reject();
3239                 self.ed_def = $.Deferred();
3240                 self.ed_def.done(function() {
3241                     self.show_error_displayer();
3242                     ignore_blur = false;
3243                     self.trigger('focused');
3244                 });
3245                 ignore_blur = true;
3246                 setTimeout(function() {
3247                     self.ed_def.resolve();
3248                     self.uned_def.reject();
3249                     self.uned_def = $.Deferred();
3250                     self.uned_def.done(function() {
3251                         self.hide_error_displayer();
3252                     });
3253                     setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3254                 }, ed_delay);
3255             } else {
3256                 self.no_ed = false;
3257                 self.ed_def.reject();
3258             }
3259         };
3260         var ignore_blur = false;
3261         this.$input.on({
3262             focusout: anyoneLoosesFocus,
3263             focus: function () { self.trigger('focused'); },
3264             autocompleteopen: function () { ignore_blur = true; },
3265             autocompleteclose: function () { ignore_blur = false; },
3266             blur: function () {
3267                 // autocomplete open
3268                 if (ignore_blur) { return; }
3269                 if (_(self.getChildren()).any(function (child) {
3270                     return child instanceof instance.web.form.AbstractFormPopup;
3271                 })) { return; }
3272                 self.trigger('blurred');
3273             }
3274         });
3275
3276         var isSelecting = false;
3277         // autocomplete
3278         this.$input.autocomplete({
3279             source: function(req, resp) {
3280                 self.get_search_result(req.term).done(function(result) {
3281                     resp(result);
3282                 });
3283             },
3284             select: function(event, ui) {
3285                 isSelecting = true;
3286                 var item = ui.item;
3287                 if (item.id) {
3288                     self.display_value = {};
3289                     self.display_value["" + item.id] = item.name;
3290                     self.reinit_value(item.id);
3291                 } else if (item.action) {
3292                     item.action();
3293                     // Cancel widget blurring, to avoid form blur event
3294                     self.trigger('focused');
3295                     return false;
3296                 }
3297             },
3298             focus: function(e, ui) {
3299                 e.preventDefault();
3300             },
3301             html: true,
3302             // disabled to solve a bug, but may cause others
3303             //close: anyoneLoosesFocus,
3304             minLength: 0,
3305             delay: 250
3306         });
3307         this.$input.autocomplete("widget").openerpClass();
3308         // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3309         this.$input.keyup(function(e) {
3310             if (e.which === 13) { // ENTER
3311                 if (isSelecting)
3312                     e.stopPropagation();
3313             }
3314             isSelecting = false;
3315         });
3316         this.setupFocus(this.$follow_button);
3317     },
3318     render_value: function(no_recurse) {
3319         var self = this;
3320         if (! this.get("value")) {
3321             this.display_string("");
3322             return;
3323         }
3324         var display = this.display_value["" + this.get("value")];
3325         if (display) {
3326             this.display_string(display);
3327             return;
3328         }
3329         if (! no_recurse) {
3330             var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3331             this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3332                 if (!data[0]) {
3333                     self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3334                     return;
3335                 }
3336                 self.display_value["" + self.get("value")] = data[0][1];
3337                 self.render_value(true);
3338             });
3339         }
3340     },
3341     display_string: function(str) {
3342         var self = this;
3343         if (!this.get("effective_readonly")) {
3344             this.$input.val(str.split("\n")[0]);
3345             this.current_display = this.$input.val();
3346             if (this.is_false()) {
3347                 this.$('.oe_m2o_cm_button').css({'display':'none'});
3348             } else {
3349                 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3350             }
3351         } else {
3352             var lines = _.escape(str).split("\n");
3353             var link = "";
3354             var follow = "";
3355             link = lines[0];
3356             follow = _.rest(lines).join("<br />");
3357             if (follow)
3358                 link += "<br />";
3359             var $link = this.$el.find('.oe_form_uri')
3360                  .unbind('click')
3361                  .html(link);
3362             if (! this.options.no_open)
3363                 $link.click(function () {
3364                     self.do_action({
3365                         type: 'ir.actions.act_window',
3366                         res_model: self.field.relation,
3367                         res_id: self.get("value"),
3368                         views: [[false, 'form']],
3369                         target: 'current',
3370                         context: self.build_context().eval(),
3371                     });
3372                     return false;
3373                  });
3374             $(".oe_form_m2o_follow", this.$el).html(follow);
3375         }
3376     },
3377     set_value: function(value_) {
3378         var self = this;
3379         if (value_ instanceof Array) {
3380             this.display_value = {};
3381             if (! this.options.always_reload) {
3382                 this.display_value["" + value_[0]] = value_[1];
3383             }
3384             value_ = value_[0];
3385         }
3386         value_ = value_ || false;
3387         this.reinit_value(value_);
3388     },
3389     get_displayed: function() {
3390         return this.display_value["" + this.get("value")];
3391     },
3392     add_id: function(id) {
3393         this.display_value = {};
3394         this.reinit_value(id);
3395     },
3396     is_false: function() {
3397         return ! this.get("value");
3398     },
3399     focus: function () {
3400         var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3401         return input ? input.focus() : false;
3402     },
3403     _quick_create: function() {
3404         this.no_ed = true;
3405         this.ed_def.reject();
3406         return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3407     },
3408     _search_create_popup: function() {
3409         this.no_ed = true;
3410         this.ed_def.reject();
3411         this.ignore_focusout = true;
3412         this.reinit_value(false);
3413         var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3414         this.ignore_focusout = false;
3415         this.no_ed = false;
3416         return res;
3417     },
3418     set_dimensions: function (height, width) {
3419         this._super(height, width);
3420         if (!this.get("effective_readonly") && this.$input)
3421             this.$input.css('height', height);
3422     }
3423 });
3424
3425 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3426     template: 'Many2OneButton',
3427     init: function(field_manager, node) {
3428         this._super.apply(this, arguments);
3429     },
3430     start: function() {
3431         this._super.apply(this, arguments);
3432         this.set_button();
3433     },
3434     set_button: function() {
3435         var self = this;
3436         if (this.$button) {
3437             this.$button.remove();
3438         }
3439         this.string = '';
3440         this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3441         this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3442         this.$button.addClass('oe_link').css({'padding':'4px'});
3443         this.$el.append(this.$button);
3444         this.$button.on('click', self.on_click);
3445     },
3446     on_click: function(ev) {
3447         var self = this;
3448         this.popup =  new instance.web.form.FormOpenPopup(this);
3449         this.popup.show_element(
3450             this.field.relation,
3451             this.get('value'),
3452             this.build_context(),
3453             {title: this.string}
3454         );
3455         this.popup.on('create_completed', self, function(r) {
3456             self.set_value(r);
3457         });
3458     },
3459     set_value: function(value_) {
3460         var self = this;
3461         if (value_ instanceof Array) {
3462             value_ = value_[0];
3463         }
3464         value_ = value_ || false;
3465         this.set('value', value_);
3466         this.set_button();
3467      },
3468 });
3469
3470 /*
3471 # Values: (0, 0,  { fields })    create
3472 #         (1, ID, { fields })    update
3473 #         (2, ID)                remove (delete)
3474 #         (3, ID)                unlink one (target id or target of relation)
3475 #         (4, ID)                link
3476 #         (5)                    unlink all (only valid for one2many)
3477 */
3478 var commands = {
3479     // (0, _, {values})
3480     CREATE: 0,
3481     'create': function (values) {
3482         return [commands.CREATE, false, values];
3483     },
3484     // (1, id, {values})
3485     UPDATE: 1,
3486     'update': function (id, values) {
3487         return [commands.UPDATE, id, values];
3488     },
3489     // (2, id[, _])
3490     DELETE: 2,
3491     'delete': function (id) {
3492         return [commands.DELETE, id, false];
3493     },
3494     // (3, id[, _]) removes relation, but not linked record itself
3495     FORGET: 3,
3496     'forget': function (id) {
3497         return [commands.FORGET, id, false];
3498     },
3499     // (4, id[, _])
3500     LINK_TO: 4,
3501     'link_to': function (id) {
3502         return [commands.LINK_TO, id, false];
3503     },
3504     // (5[, _[, _]])
3505     DELETE_ALL: 5,
3506     'delete_all': function () {
3507         return [5, false, false];
3508     },
3509     // (6, _, ids) replaces all linked records with provided ids
3510     REPLACE_WITH: 6,
3511     'replace_with': function (ids) {
3512         return [6, false, ids];
3513     }
3514 };
3515 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3516     multi_selection: false,
3517     disable_utility_classes: true,
3518     init: function(field_manager, node) {
3519         this._super(field_manager, node);
3520         lazy_build_o2m_kanban_view();
3521         this.is_loaded = $.Deferred();
3522         this.initial_is_loaded = this.is_loaded;
3523         this.form_last_update = $.Deferred();
3524         this.init_form_last_update = this.form_last_update;
3525         this.is_started = false;
3526         this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3527         this.dataset.o2m = this;
3528         this.dataset.parent_view = this.view;
3529         this.dataset.child_name = this.name;
3530         var self = this;
3531         this.dataset.on('dataset_changed', this, function() {
3532             self.trigger_on_change();
3533         });
3534         this.set_value([]);
3535     },
3536     start: function() {
3537         this._super.apply(this, arguments);
3538         this.$el.addClass('oe_form_field oe_form_field_one2many');
3539
3540         var self = this;
3541
3542         self.load_views();
3543         this.is_loaded.done(function() {
3544             self.on("change:effective_readonly", self, function() {
3545                 self.is_loaded = self.is_loaded.then(function() {
3546                     self.viewmanager.destroy();
3547                     return $.when(self.load_views()).done(function() {
3548                         self.reload_current_view();
3549                     });
3550                 });
3551             });
3552         });
3553         this.is_started = true;
3554         this.reload_current_view();
3555     },
3556     trigger_on_change: function() {
3557         this.trigger('changed_value');
3558     },
3559     load_views: function() {
3560         var self = this;
3561
3562         var modes = this.node.attrs.mode;
3563         modes = !!modes ? modes.split(",") : ["tree"];
3564         var views = [];
3565         _.each(modes, function(mode) {
3566             if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3567                 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3568             }
3569             var view = {
3570                 view_id: false,
3571                 view_type: mode == "tree" ? "list" : mode,
3572                 options: {}
3573             };
3574             if (self.field.views && self.field.views[mode]) {
3575                 view.embedded_view = self.field.views[mode];
3576             }
3577             if(view.view_type === "list") {
3578                 _.extend(view.options, {
3579                     addable: null,
3580                     selectable: self.multi_selection,
3581                     sortable: true,
3582                     import_enabled: false,
3583                     deletable: true
3584                 });
3585                 if (self.get("effective_readonly")) {
3586                     _.extend(view.options, {
3587                         deletable: null,
3588                         reorderable: false,
3589                     });
3590                 }
3591             } else if (view.view_type === "form") {
3592                 if (self.get("effective_readonly")) {
3593                     view.view_type = 'form';
3594                 }
3595                 _.extend(view.options, {
3596                     not_interactible_on_create: true,
3597                 });
3598             } else if (view.view_type === "kanban") {
3599                 _.extend(view.options, {
3600                     confirm_on_delete: false,
3601                 });
3602                 if (self.get("effective_readonly")) {
3603                     _.extend(view.options, {
3604                         action_buttons: false,
3605                         quick_creatable: false,
3606                         creatable: false,
3607                         read_only_mode: true,
3608                     });
3609                 }
3610             }
3611             views.push(view);
3612         });
3613         this.views = views;
3614
3615         this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3616         this.viewmanager.o2m = self;
3617         var once = $.Deferred().done(function() {
3618             self.init_form_last_update.resolve();
3619         });
3620         var def = $.Deferred().done(function() {
3621             self.initial_is_loaded.resolve();
3622         });
3623         this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3624             controller.o2m = self;
3625             if (view_type == "list") {
3626                 if (self.get("effective_readonly")) {
3627                     controller.on('edit:before', self, function (e) {
3628                         e.cancel = true;
3629                     });
3630                     _(controller.columns).find(function (column) {
3631                         if (!(column instanceof instance.web.list.Handle)) {
3632                             return false;
3633                         }
3634                         column.modifiers.invisible = true;
3635                         return true;
3636                     });
3637                 }
3638             } else if (view_type === "form") {
3639                 if (self.get("effective_readonly")) {
3640                     $(".oe_form_buttons", controller.$el).children().remove();
3641                 }
3642                 controller.on("load_record", self, function(){
3643                      once.resolve();
3644                  });
3645                 controller.on('pager_action_executed',self,self.save_any_view);
3646             } else if (view_type == "graph") {
3647                 self.reload_current_view()
3648             }
3649             def.resolve();
3650         });
3651         this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3652             $.when(self.save_any_view()).done(function() {
3653                 if (n_mode === "list") {
3654                     $.async_when().done(function() {
3655                         self.reload_current_view();
3656                     });
3657                 }
3658             });
3659         });
3660         $.async_when().done(function () {
3661             self.viewmanager.appendTo(self.$el);
3662         });
3663         return def;
3664     },
3665     reload_current_view: function() {
3666         var self = this;
3667         return self.is_loaded = self.is_loaded.then(function() {
3668             var active_view = self.viewmanager.active_view;
3669             var view = self.viewmanager.views[active_view].controller;
3670             if(active_view === "list") {
3671                 return view.reload_content();
3672             } else if (active_view === "form") {
3673                 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3674                     self.dataset.index = 0;
3675                 }
3676                 var act = function() {
3677                     return view.do_show();
3678                 };
3679                 self.form_last_update = self.form_last_update.then(act, act);
3680                 return self.form_last_update;
3681             } else if (view.do_search) {
3682                 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3683             }
3684         }, undefined);
3685     },
3686     set_value: function(value_) {
3687         value_ = value_ || [];
3688         var self = this;
3689         this.dataset.reset_ids([]);
3690         if(value_.length >= 1 && value_[0] instanceof Array) {
3691             var ids = [];
3692             _.each(value_, function(command) {
3693                 var obj = {values: command[2]};
3694                 switch (command[0]) {
3695                     case commands.CREATE:
3696                         obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3697                         obj.defaults = {};
3698                         self.dataset.to_create.push(obj);
3699                         self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3700                         ids.push(obj.id);
3701                         return;
3702                     case commands.UPDATE:
3703                         obj['id'] = command[1];
3704                         self.dataset.to_write.push(obj);
3705                         self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3706                         ids.push(obj.id);
3707                         return;
3708                     case commands.DELETE:
3709                         self.dataset.to_delete.push({id: command[1]});
3710                         return;
3711                     case commands.LINK_TO:
3712                         ids.push(command[1]);
3713                         return;
3714                     case commands.DELETE_ALL:
3715                         self.dataset.delete_all = true;
3716                         return;
3717                 }
3718             });
3719             this._super(ids);
3720             this.dataset.set_ids(ids);
3721         } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3722             var ids = [];
3723             this.dataset.delete_all = true;
3724             _.each(value_, function(command) {
3725                 var obj = {values: command};
3726                 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3727                 obj.defaults = {};
3728                 self.dataset.to_create.push(obj);
3729                 self.dataset.cache.push(_.clone(obj));
3730                 ids.push(obj.id);
3731             });
3732             this._super(ids);
3733             this.dataset.set_ids(ids);
3734         } else {
3735             this._super(value_);
3736             this.dataset.reset_ids(value_);
3737         }
3738         if (this.dataset.index === null && this.dataset.ids.length > 0) {
3739             this.dataset.index = 0;
3740         }
3741         this.trigger_on_change();
3742         if (this.is_started) {
3743             return self.reload_current_view();
3744         } else {
3745             return $.when();
3746         }
3747     },
3748     get_value: function() {
3749         var self = this;
3750         if (!this.dataset)
3751             return [];
3752         var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3753         val = val.concat(_.map(this.dataset.ids, function(id) {
3754             var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3755             if (alter_order) {
3756                 return commands.create(alter_order.values);
3757             }
3758             alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3759             if (alter_order) {
3760                 return commands.update(alter_order.id, alter_order.values);
3761             }
3762             return commands.link_to(id);
3763         }));
3764         return val.concat(_.map(
3765             this.dataset.to_delete, function(x) {
3766                 return commands['delete'](x.id);}));
3767     },
3768     commit_value: function() {
3769         return this.save_any_view();
3770     },
3771     save_any_view: function() {
3772         if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3773             this.viewmanager.views[this.viewmanager.active_view] &&
3774             this.viewmanager.views[this.viewmanager.active_view].controller) {
3775             var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3776             if (this.viewmanager.active_view === "form") {
3777                 if (!view.is_initialized.state() === 'resolved') {
3778                     return $.when(false);
3779                 }
3780                 return $.when(view.save());
3781             } else if (this.viewmanager.active_view === "list") {
3782                 return $.when(view.ensure_saved());
3783             }
3784         }
3785         return $.when(false);
3786     },
3787     is_syntax_valid: function() {
3788         if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3789             return true;
3790         var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3791         switch (this.viewmanager.active_view) {
3792         case 'form':
3793             return _(view.fields).chain()
3794                 .invoke('is_valid')
3795                 .all(_.identity)
3796                 .value();
3797             break;
3798         case 'list':
3799             return view.is_valid();
3800         }
3801         return true;
3802     },
3803 });
3804
3805 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3806     template: 'One2Many.viewmanager',
3807     init: function(parent, dataset, views, flags) {
3808         this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3809         this.registry = this.registry.extend({
3810             list: 'instance.web.form.One2ManyListView',
3811             form: 'instance.web.form.One2ManyFormView',
3812             kanban: 'instance.web.form.One2ManyKanbanView',
3813         });
3814         this.__ignore_blur = false;
3815     },
3816     switch_mode: function(mode, unused) {
3817         if (mode !== 'form') {
3818             return this._super(mode, unused);
3819         }
3820         var self = this;
3821         var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3822         var pop = new instance.web.form.FormOpenPopup(this);
3823         pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3824             title: _t("Open: ") + self.o2m.string,
3825             create_function: function(data, options) {
3826                 return self.o2m.dataset.create(data, options).done(function(r) {
3827                     self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3828                     self.o2m.dataset.trigger("dataset_changed", r);
3829                 });
3830             },
3831             write_function: function(id, data, options) {
3832                 return self.o2m.dataset.write(id, data, {}).done(function() {
3833                     self.o2m.reload_current_view();
3834                 });
3835             },
3836             alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3837             parent_view: self.o2m.view,
3838             child_name: self.o2m.name,
3839             read_function: function() {
3840                 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3841             },
3842             form_view_options: {'not_interactible_on_create':true},
3843             readonly: self.o2m.get("effective_readonly")
3844         });
3845         pop.on("elements_selected", self, function() {
3846             self.o2m.reload_current_view();
3847         });
3848     },
3849 });
3850
3851 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3852     get_context: function() {
3853         this.context = this.o2m.build_context();
3854         return this.context;
3855     }
3856 });
3857
3858 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3859     _template: 'One2Many.listview',
3860     init: function (parent, dataset, view_id, options) {
3861         this._super(parent, dataset, view_id, _.extend(options || {}, {
3862             GroupsType: instance.web.form.One2ManyGroups,
3863             ListType: instance.web.form.One2ManyList
3864         }));
3865         this.on('edit:after', this, this.proxy('_after_edit'));
3866         this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3867
3868         this.records
3869             .bind('add', this.proxy("changed_records"))
3870             .bind('edit', this.proxy("changed_records"))
3871             .bind('remove', this.proxy("changed_records"));
3872     },
3873     start: function () {
3874         var ret = this._super();
3875         this.$el
3876             .off('mousedown.handleButtons')
3877             .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
3878         return ret;
3879     },
3880     changed_records: function () {
3881         this.o2m.trigger_on_change();
3882     },
3883     is_valid: function () {
3884         var editor = this.editor;
3885         var form = editor.form;
3886         // If no edition is pending, the listview can not be invalid (?)
3887         if (!editor.record) {
3888             return true
3889         }
3890         // If the form has not been modified, the view can only be valid
3891         // NB: is_dirty will also be set on defaults/onchanges/whatever?
3892         // oe_form_dirty seems to only be set on actual user actions
3893         if (!form.$el.is('.oe_form_dirty')) {
3894             return true;
3895         }
3896         this.o2m._dirty_flag = true;
3897
3898         // Otherwise validate internal form
3899         return _(form.fields).chain()
3900             .invoke(function () {
3901                 this._check_css_flags();
3902                 return this.is_valid();
3903             })
3904             .all(_.identity)
3905             .value();
3906     },
3907     do_add_record: function () {
3908         if (this.editable()) {
3909             this._super.apply(this, arguments);
3910         } else {
3911             var self = this;
3912             var pop = new instance.web.form.SelectCreatePopup(this);
3913             pop.select_element(
3914                 self.o2m.field.relation,
3915                 {
3916                     title: _t("Create: ") + self.o2m.string,
3917                     initial_view: "form",
3918                     alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3919                     create_function: function(data, options) {
3920                         return self.o2m.dataset.create(data, options).done(function(r) {
3921                             self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3922                             self.o2m.dataset.trigger("dataset_changed", r);
3923                         });
3924                     },
3925                     read_function: function() {
3926                         return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3927                     },
3928                     parent_view: self.o2m.view,
3929                     child_name: self.o2m.name,
3930                     form_view_options: {'not_interactible_on_create':true}
3931                 },
3932                 self.o2m.build_domain(),
3933                 self.o2m.build_context()
3934             );
3935             pop.on("elements_selected", self, function() {
3936                 self.o2m.reload_current_view();
3937             });
3938         }
3939     },
3940     do_activate_record: function(index, id) {
3941         var self = this;
3942         var pop = new instance.web.form.FormOpenPopup(self);
3943         pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3944             title: _t("Open: ") + self.o2m.string,
3945             write_function: function(id, data) {
3946                 return self.o2m.dataset.write(id, data, {}).done(function() {
3947                     self.o2m.reload_current_view();
3948                 });
3949             },
3950             alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3951             parent_view: self.o2m.view,
3952             child_name: self.o2m.name,
3953             read_function: function() {
3954                 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3955             },
3956             form_view_options: {'not_interactible_on_create':true},
3957             readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3958         });
3959     },
3960     do_button_action: function (name, id, callback) {
3961         if (!_.isNumber(id)) {
3962             instance.webclient.notification.warn(
3963                 _t("Action Button"),
3964                 _t("The o2m record must be saved before an action can be used"));
3965             return;
3966         }
3967         var parent_form = this.o2m.view;
3968         var self = this;
3969         this.ensure_saved().then(function () {
3970             if (parent_form)
3971                 return parent_form.save();
3972             else
3973                 return $.when();
3974         }).done(function () {
3975             var ds = self.o2m.dataset;
3976             var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
3977                 return value.length;
3978             });
3979             if (!self.o2m.options.reload_on_button && !cached_records) {
3980                 self.handle_button(name, id, callback);
3981             }else {
3982                 self.handle_button(name, id, function(){
3983                     self.o2m.view.reload();
3984                 });
3985             }
3986         });
3987     },
3988
3989     _after_edit: function () {
3990         this.__ignore_blur = false;
3991         this.editor.form.on('blurred', this, this._on_form_blur);
3992
3993         // The form's blur thing may be jiggered during the edition setup,
3994         // potentially leading to the o2m instasaving the row. Cancel any
3995         // blurring triggered the edition startup here
3996         this.editor.form.widgetFocused();
3997     },
3998     _before_unedit: function () {
3999         this.editor.form.off('blurred', this, this._on_form_blur);
4000     },
4001     _button_down: function () {
4002         // If a button is clicked (usually some sort of action button), it's
4003         // the button's responsibility to ensure the editable list is in the
4004         // correct state -> ignore form blurring
4005         this.__ignore_blur = true;
4006     },
4007     /**
4008      * Handles blurring of the nested form (saves the currently edited row),
4009      * unless the flag to ignore the event is set to ``true``
4010      *
4011      * Makes the internal form go away
4012      */
4013     _on_form_blur: function () {
4014         if (this.__ignore_blur) {
4015             this.__ignore_blur = false;
4016             return;
4017         }
4018         // FIXME: why isn't there an API for this?
4019         if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4020             this.ensure_saved();
4021             return;
4022         }
4023         this.cancel_edition();
4024     },
4025     keypress_ENTER: function () {
4026         // blurring caused by hitting the [Return] key, should skip the
4027         // autosave-on-blur and let the handler for [Return] do its thing (save
4028         // the current row *anyway*, then create a new one/edit the next one)
4029         this.__ignore_blur = true;
4030         this._super.apply(this, arguments);
4031     },
4032     do_delete: function (ids) {
4033         var confirm = window.confirm;
4034         window.confirm = function () { return true; };
4035         try {
4036             return this._super(ids);
4037         } finally {
4038             window.confirm = confirm;
4039         }
4040     },
4041     reload_record: function (record) {
4042         // Evict record.id from cache to ensure it will be reloaded correctly
4043         this.dataset.evict_record(record.get('id'));
4044
4045         return this._super(record);
4046     }
4047 });
4048 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4049     setup_resequence_rows: function () {
4050         if (!this.view.o2m.get('effective_readonly')) {
4051             this._super.apply(this, arguments);
4052         }
4053     }
4054 });
4055 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4056     pad_table_to: function (count) {
4057         if (!this.view.is_action_enabled('create')) {
4058             this._super(count);
4059         } else {
4060             this._super(count > 0 ? count - 1 : 0);
4061         }
4062
4063         // magical invocation of wtf does that do
4064         if (this.view.o2m.get('effective_readonly')) {
4065             return;
4066         }
4067
4068         var self = this;
4069         var columns = _(this.columns).filter(function (column) {
4070             return column.invisible !== '1';
4071         }).length;
4072         if (this.options.selectable) { columns++; }
4073         if (this.options.deletable) { columns++; }
4074
4075         if (!this.view.is_action_enabled('create')) {
4076             return;
4077         }
4078
4079         var $cell = $('<td>', {
4080             colspan: columns,
4081             'class': 'oe_form_field_one2many_list_row_add'
4082         }).append(
4083             $('<a>', {href: '#'}).text(_t("Add an item"))
4084                 .mousedown(function () {
4085                     // FIXME: needs to be an official API somehow
4086                     if (self.view.editor.is_editing()) {
4087                         self.view.__ignore_blur = true;
4088                     }
4089                 })
4090                 .click(function (e) {
4091                     e.preventDefault();
4092                     e.stopPropagation();
4093                     // FIXME: there should also be an API for that one
4094                     if (self.view.editor.form.__blur_timeout) {
4095                         clearTimeout(self.view.editor.form.__blur_timeout);
4096                         self.view.editor.form.__blur_timeout = false;
4097                     }
4098                     self.view.ensure_saved().done(function () {
4099                         self.view.do_add_record();
4100                     });
4101                 }));
4102
4103         var $padding = this.$current.find('tr:not([data-id]):first');
4104         var $newrow = $('<tr>').append($cell);
4105         if ($padding.length) {
4106             $padding.before($newrow);
4107         } else {
4108             this.$current.append($newrow)
4109         }
4110     }
4111 });
4112
4113 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4114     form_template: 'One2Many.formview',
4115     load_form: function(data) {
4116         this._super(data);
4117         var self = this;
4118         this.$buttons.find('button.oe_form_button_create').click(function() {
4119             self.save().done(self.on_button_new);
4120         });
4121     },
4122     do_notify_change: function() {
4123         if (this.dataset.parent_view) {
4124             this.dataset.parent_view.do_notify_change();
4125         } else {
4126             this._super.apply(this, arguments);
4127         }
4128     }
4129 });
4130
4131 var lazy_build_o2m_kanban_view = function() {
4132     if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4133         return;
4134     instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4135     });
4136 };
4137
4138 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4139     template: "FieldMany2ManyTags",
4140     init: function() {
4141         this._super.apply(this, arguments);
4142         instance.web.form.CompletionFieldMixin.init.call(this);
4143         this.set({"value": []});
4144         this._display_orderer = new instance.web.DropMisordered();
4145         this._drop_shown = false;
4146     },
4147     initialize_content: function() {
4148         if (this.get("effective_readonly"))
4149             return;
4150         var self = this;
4151         var ignore_blur = false;
4152         self.$text = this.$("textarea");
4153         self.$text.textext({
4154             plugins : 'tags arrow autocomplete',
4155             autocomplete: {
4156                 render: function(suggestion) {
4157                     return $('<span class="text-label"/>').
4158                              data('index', suggestion['index']).html(suggestion['label']);
4159                 }
4160             },
4161             ext: {
4162                 autocomplete: {
4163                     selectFromDropdown: function() {
4164                         this.trigger('hideDropdown');
4165                         var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4166                         var data = self.search_result[index];
4167                         if (data.id) {
4168                             self.add_id(data.id);
4169                         } else {
4170                             ignore_blur = true;
4171                             data.action();
4172                         }
4173                         this.trigger('setSuggestions', {result : []});
4174                     },
4175                 },
4176                 tags: {
4177                     isTagAllowed: function(tag) {
4178                         return !!tag.name;
4179
4180                     },
4181                     removeTag: function(tag) {
4182                         var id = tag.data("id");
4183                         self.set({"value": _.without(self.get("value"), id)});
4184                     },
4185                     renderTag: function(stuff) {
4186                         return $.fn.textext.TextExtTags.prototype.renderTag.
4187                             call(this, stuff).data("id", stuff.id);
4188                     },
4189                 },
4190                 itemManager: {
4191                     itemToString: function(item) {
4192                         return item.name;
4193                     },
4194                 },
4195                 core: {
4196                     onSetInputData: function(e, data) {
4197                         if (data == '') {
4198                             this._plugins.autocomplete._suggestions = null;
4199                         }
4200                         this.input().val(data);
4201                     },
4202                 },
4203             },
4204         }).bind('getSuggestions', function(e, data) {
4205             var _this = this;
4206             var str = !!data ? data.query || '' : '';
4207             self.get_search_result(str).done(function(result) {
4208                 self.search_result = result;
4209                 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4210                     return _.extend(el, {index:i});
4211                 })});
4212             });
4213         }).bind('hideDropdown', function() {
4214             self._drop_shown = false;
4215         }).bind('showDropdown', function() {
4216             self._drop_shown = true;
4217         });
4218         self.tags = self.$text.textext()[0].tags();
4219         self.$text
4220             .focusin(function () {
4221                 self.trigger('focused');
4222                 ignore_blur = false;
4223             })
4224             .focusout(function() {
4225                 self.$text.trigger("setInputData", "");
4226                 if (!ignore_blur) {
4227                     self.trigger('blurred');
4228                 }
4229             }).keydown(function(e) {
4230                 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4231                     self.$text.textext()[0].autocomplete().selectFromDropdown();
4232                 }
4233             });
4234     },
4235     set_value: function(value_) {
4236         value_ = value_ || [];
4237         if (value_.length >= 1 && value_[0] instanceof Array) {
4238             value_ = value_[0][2];
4239         }
4240         this._super(value_);
4241     },
4242     is_false: function() {
4243         return _(this.get("value")).isEmpty();
4244     },
4245     get_value: function() {
4246         var tmp = [commands.replace_with(this.get("value"))];
4247         return tmp;
4248     },
4249     get_search_blacklist: function() {
4250         return this.get("value");
4251     },
4252     render_value: function() {
4253         var self = this;
4254         var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4255         var values = self.get("value");
4256         var handle_names = function(data) {
4257             if (self.isDestroyed())
4258                 return;
4259             var indexed = {};
4260             _.each(data, function(el) {
4261                 indexed[el[0]] = el;
4262             });
4263             data = _.map(values, function(el) { return indexed[el]; });
4264             if (! self.get("effective_readonly")) {
4265                 self.tags.containerElement().children().remove();
4266                 self.$('textarea').css("padding-left", "3px");
4267                 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4268             } else {
4269                 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4270             }
4271         };
4272         if (! values || values.length > 0) {
4273             return this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4274         } else {
4275             handle_names([]);
4276         }
4277     },
4278     add_id: function(id) {
4279         this.set({'value': _.uniq(this.get('value').concat([id]))});
4280     },
4281     focus: function () {
4282         var input = this.$text && this.$text[0];
4283         return input ? input.focus() : false;
4284     },
4285 });
4286
4287 /**
4288     widget options:
4289     - reload_on_button: Reload the whole form view if click on a button in a list view.
4290         If you see this options, do not use it, it's basically a dirty hack to make one
4291         precise o2m to behave the way we want.
4292 */
4293 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4294     multi_selection: false,
4295     disable_utility_classes: true,
4296     init: function(field_manager, node) {
4297         this._super(field_manager, node);
4298         this.is_loaded = $.Deferred();
4299         this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4300         this.dataset.m2m = this;
4301         var self = this;
4302         this.dataset.on('unlink', self, function(ids) {
4303             self.dataset_changed();
4304         });
4305         this.set_value([]);
4306         this.list_dm = new instance.web.DropMisordered();
4307         this.render_value_dm = new instance.web.DropMisordered();
4308     },
4309     initialize_content: function() {
4310         var self = this;
4311
4312         this.$el.addClass('oe_form_field oe_form_field_many2many');
4313
4314         this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4315                     'addable': this.get("effective_readonly") ? null : _t("Add"),
4316                     'deletable': this.get("effective_readonly") ? false : true,
4317                     'selectable': this.multi_selection,
4318                     'sortable': false,
4319                     'reorderable': false,
4320                     'import_enabled': false,
4321             });
4322         var embedded = (this.field.views || {}).tree;
4323         if (embedded) {
4324             this.list_view.set_embedded_view(embedded);
4325         }
4326         this.list_view.m2m_field = this;
4327         var loaded = $.Deferred();
4328         this.list_view.on("list_view_loaded", this, function() {
4329             loaded.resolve();
4330         });
4331         this.list_view.appendTo(this.$el);
4332
4333         var old_def = self.is_loaded;
4334         self.is_loaded = $.Deferred().done(function() {
4335             old_def.resolve();
4336         });
4337         this.list_dm.add(loaded).then(function() {
4338             self.is_loaded.resolve();
4339         });
4340     },
4341     destroy_content: function() {
4342         this.list_view.destroy();
4343         this.list_view = undefined;
4344     },
4345     set_value: function(value_) {
4346         value_ = value_ || [];
4347         if (value_.length >= 1 && value_[0] instanceof Array) {
4348             value_ = value_[0][2];
4349         }
4350         this._super(value_);
4351     },
4352     get_value: function() {
4353         return [commands.replace_with(this.get('value'))];
4354     },
4355     is_false: function () {
4356         return _(this.get("value")).isEmpty();
4357     },
4358     render_value: function() {
4359         var self = this;
4360         this.dataset.set_ids(this.get("value"));
4361         this.render_value_dm.add(this.is_loaded).then(function() {
4362             return self.list_view.reload_content();
4363         });
4364     },
4365     dataset_changed: function() {
4366         this.internal_set_value(this.dataset.ids);
4367     },
4368 });
4369
4370 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4371     get_context: function() {
4372         this.context = this.m2m.build_context();
4373         return this.context;
4374     }
4375 });
4376
4377 /**
4378  * @class
4379  * @extends instance.web.ListView
4380  */
4381 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4382     do_add_record: function () {
4383         var pop = new instance.web.form.SelectCreatePopup(this);
4384         pop.select_element(
4385             this.model,
4386             {
4387                 title: _t("Add: ") + this.m2m_field.string,
4388                 no_create: this.m2m_field.options.no_create,
4389             },
4390             new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4391             this.m2m_field.build_context()
4392         );
4393         var self = this;
4394         pop.on("elements_selected", self, function(element_ids) {
4395             var reload = false;
4396             _(element_ids).each(function (id) {
4397                 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4398                     self.dataset.set_ids(self.dataset.ids.concat([id]));
4399                     self.m2m_field.dataset_changed();
4400                     reload = true;
4401                 }
4402             });
4403             if (reload) {
4404                 self.reload_content();
4405             }
4406         });
4407     },
4408     do_activate_record: function(index, id) {
4409         var self = this;
4410         var pop = new instance.web.form.FormOpenPopup(this);
4411         pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4412             title: _t("Open: ") + this.m2m_field.string,
4413             readonly: this.getParent().get("effective_readonly")
4414         });
4415         pop.on('write_completed', self, self.reload_content);
4416     },
4417     do_button_action: function(name, id, callback) {
4418         var self = this;
4419         var _sup = _.bind(this._super, this);
4420         if (! this.m2m_field.options.reload_on_button) {
4421             return _sup(name, id, callback);
4422         } else {
4423             return this.m2m_field.view.save().then(function() {
4424                 return _sup(name, id, function() {
4425                     self.m2m_field.view.reload();
4426                 });
4427             });
4428         }
4429      },
4430     is_action_enabled: function () { return true; },
4431 });
4432
4433 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4434     disable_utility_classes: true,
4435     init: function(field_manager, node) {
4436         this._super(field_manager, node);
4437         instance.web.form.CompletionFieldMixin.init.call(this);
4438         m2m_kanban_lazy_init();
4439         this.is_loaded = $.Deferred();
4440         this.initial_is_loaded = this.is_loaded;
4441
4442         var self = this;
4443         this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4444         this.dataset.m2m = this;
4445         this.dataset.on('unlink', self, function(ids) {
4446             self.dataset_changed();
4447         });
4448     },
4449     start: function() {
4450         this._super.apply(this, arguments);
4451
4452         var self = this;
4453
4454         self.load_view();
4455         self.on("change:effective_readonly", self, function() {
4456             self.is_loaded = self.is_loaded.then(function() {
4457                 self.kanban_view.destroy();
4458                 return $.when(self.load_view()).done(function() {
4459                     self.render_value();
4460                 });
4461             });
4462         });
4463     },
4464     set_value: function(value_) {
4465         value_ = value_ || [];
4466         if (value_.length >= 1 && value_[0] instanceof Array) {
4467             value_ = value_[0][2];
4468         }
4469         this._super(value_);
4470     },
4471     get_value: function() {
4472         return [commands.replace_with(this.get('value'))];
4473     },
4474     load_view: function() {
4475         var self = this;
4476         this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4477                     'create_text': _t("Add"),
4478                     'creatable': self.get("effective_readonly") ? false : true,
4479                     'quick_creatable': self.get("effective_readonly") ? false : true,
4480                     'read_only_mode': self.get("effective_readonly") ? true : false,
4481                     'confirm_on_delete': false,
4482             });
4483         var embedded = (this.field.views || {}).kanban;
4484         if (embedded) {
4485             this.kanban_view.set_embedded_view(embedded);
4486         }
4487         this.kanban_view.m2m = this;
4488         var loaded = $.Deferred();
4489         this.kanban_view.on("kanban_view_loaded",self,function() {
4490             self.initial_is_loaded.resolve();
4491             loaded.resolve();
4492         });
4493         this.kanban_view.on('switch_mode', this, this.open_popup);
4494         $.async_when().done(function () {
4495             self.kanban_view.appendTo(self.$el);
4496         });
4497         return loaded;
4498     },
4499     render_value: function() {
4500         var self = this;
4501         this.dataset.set_ids(this.get("value"));
4502         this.is_loaded = this.is_loaded.then(function() {
4503             return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4504         });
4505     },
4506     dataset_changed: function() {
4507         this.set({'value': this.dataset.ids});
4508     },
4509     open_popup: function(type, unused) {
4510         if (type !== "form")
4511             return;
4512         var self = this;
4513         if (this.dataset.index === null) {
4514             var pop = new instance.web.form.SelectCreatePopup(this);
4515             pop.select_element(
4516                 this.field.relation,
4517                 {
4518                     title: _t("Add: ") + this.string
4519                 },
4520                 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4521                 this.build_context()
4522             );
4523             pop.on("elements_selected", self, function(element_ids) {
4524                 _.each(element_ids, function(one_id) {
4525                     if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4526                         self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4527                         self.dataset_changed();
4528                         self.render_value();
4529                     }
4530                 });
4531             });
4532         } else {
4533             var id = self.dataset.ids[self.dataset.index];
4534             var pop = new instance.web.form.FormOpenPopup(this);
4535             pop.show_element(self.field.relation, id, self.build_context(), {
4536                 title: _t("Open: ") + self.string,
4537                 write_function: function(id, data, options) {
4538                     return self.dataset.write(id, data, {}).done(function() {
4539                         self.render_value();
4540                     });
4541                 },
4542                 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4543                 parent_view: self.view,
4544                 child_name: self.name,
4545                 readonly: self.get("effective_readonly")
4546             });
4547         }
4548     },
4549     add_id: function(id) {
4550         this.quick_create.add_id(id);
4551     },
4552 });
4553
4554 function m2m_kanban_lazy_init() {
4555 if (instance.web.form.Many2ManyKanbanView)
4556     return;
4557 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4558     quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4559     _is_quick_create_enabled: function() {
4560         return this._super() && ! this.group_by;
4561     },
4562 });
4563 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4564     template: 'Many2ManyKanban.quick_create',
4565
4566     /**
4567      * close_btn: If true, the widget will display a "Close" button able to trigger
4568      * a "close" event.
4569      */
4570     init: function(parent, dataset, context, buttons) {
4571         this._super(parent);
4572         this.m2m = this.getParent().view.m2m;
4573         this.m2m.quick_create = this;
4574         this._dataset = dataset;
4575         this._buttons = buttons || false;
4576         this._context = context || {};
4577     },
4578     start: function () {
4579         var self = this;
4580         self.$text = this.$el.find('input').css("width", "200px");
4581         self.$text.textext({
4582             plugins : 'arrow autocomplete',
4583             autocomplete: {
4584                 render: function(suggestion) {
4585                     return $('<span class="text-label"/>').
4586                              data('index', suggestion['index']).html(suggestion['label']);
4587                 }
4588             },
4589             ext: {
4590                 autocomplete: {
4591                     selectFromDropdown: function() {
4592                         $(this).trigger('hideDropdown');
4593                         var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4594                         var data = self.search_result[index];
4595                         if (data.id) {
4596                             self.add_id(data.id);
4597                         } else {
4598                             data.action();
4599                         }
4600                     },
4601                 },
4602                 itemManager: {
4603                     itemToString: function(item) {
4604                         return item.name;
4605                     },
4606                 },
4607             },
4608         }).bind('getSuggestions', function(e, data) {
4609             var _this = this;
4610             var str = !!data ? data.query || '' : '';
4611             self.m2m.get_search_result(str).done(function(result) {
4612                 self.search_result = result;
4613                 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4614                     return _.extend(el, {index:i});
4615                 })});
4616             });
4617         });
4618         self.$text.focusout(function() {
4619             self.$text.val("");
4620         });
4621     },
4622     focus: function() {
4623         this.$text[0].focus();
4624     },
4625     add_id: function(id) {
4626         var self = this;
4627         self.$text.val("");
4628         self.trigger('added', id);
4629         this.m2m.dataset_changed();
4630     },
4631 });
4632 }
4633
4634 /**
4635  * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4636  */
4637 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4638     template: "AbstractFormPopup.render",
4639     /**
4640      *  options:
4641      *  -readonly: only applicable when not in creation mode, default to false
4642      * - alternative_form_view
4643      * - view_id
4644      * - write_function
4645      * - read_function
4646      * - create_function
4647      * - parent_view
4648      * - child_name
4649      * - form_view_options
4650      */
4651     init_popup: function(model, row_id, domain, context, options) {
4652         this.row_id = row_id;
4653         this.model = model;
4654         this.domain = domain || [];
4655         this.context = context || {};
4656         this.options = options;
4657         _.defaults(this.options, {
4658         });
4659     },
4660     init_dataset: function() {
4661         var self = this;
4662         this.created_elements = [];
4663         this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4664         this.dataset.read_function = this.options.read_function;
4665         this.dataset.create_function = function(data, options, sup) {
4666             var fct = self.options.create_function || sup;
4667             return fct.call(this, data, options).done(function(r) {
4668                 self.trigger('create_completed saved', r);
4669                 self.created_elements.push(r);
4670             });
4671         };
4672         this.dataset.write_function = function(id, data, options, sup) {
4673             var fct = self.options.write_function || sup;
4674             return fct.call(this, id, data, options).done(function(r) {
4675                 self.trigger('write_completed saved', r);
4676             });
4677         };
4678         this.dataset.parent_view = this.options.parent_view;
4679         this.dataset.child_name = this.options.child_name;
4680     },
4681     display_popup: function() {
4682         var self = this;
4683         this.renderElement();
4684         var dialog = new instance.web.Dialog(this, {
4685             min_width: '800px',
4686             dialogClass: 'oe_act_window',
4687             close: function() {
4688                 self.check_exit(true);
4689             },
4690             title: this.options.title || "",
4691         }, this.$el).open();
4692         this.$buttonpane = dialog.$buttons;
4693         this.start();
4694     },
4695     setup_form_view: function() {
4696         var self = this;
4697         if (this.row_id) {
4698             this.dataset.ids = [this.row_id];
4699             this.dataset.index = 0;
4700         } else {
4701             this.dataset.index = null;
4702         }
4703         var options = _.clone(self.options.form_view_options) || {};
4704         if (this.row_id !== null) {
4705             options.initial_mode = this.options.readonly ? "view" : "edit";
4706         }
4707         _.extend(options, {
4708             $buttons: this.$buttonpane,
4709         });
4710         this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4711         if (this.options.alternative_form_view) {
4712             this.view_form.set_embedded_view(this.options.alternative_form_view);
4713         }
4714         this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4715         this.view_form.on("form_view_loaded", self, function() {
4716             var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4717             self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4718                 multi_select: multi_select,
4719                 readonly: self.row_id !== null && self.options.readonly,
4720             }));
4721             var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4722             $snbutton.click(function() {
4723                 $.when(self.view_form.save()).done(function() {
4724                     self.view_form.reload_mutex.exec(function() {
4725                         self.view_form.on_button_new();
4726                     });
4727                 });
4728             });
4729             var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4730             $sbutton.click(function() {
4731                 $.when(self.view_form.save()).done(function() {
4732                     self.view_form.reload_mutex.exec(function() {
4733                         self.check_exit();
4734                     });
4735                 });
4736             });
4737             var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4738             $cbutton.click(function() {
4739                 self.view_form.trigger('on_button_cancel');
4740                 self.check_exit();
4741             });
4742             self.view_form.do_show();
4743         });
4744     },
4745     select_elements: function(element_ids) {
4746         this.trigger("elements_selected", element_ids);
4747     },
4748     check_exit: function(no_destroy) {
4749         if (this.created_elements.length > 0) {
4750             this.select_elements(this.created_elements);
4751             this.created_elements = [];
4752         }
4753         this.trigger('closed');
4754         this.destroy();
4755     },
4756     destroy: function () {
4757         this.trigger('closed');
4758         if (this.$el.is(":data(dialog)")) {
4759             this.$el.dialog('close');
4760         }
4761         this._super();
4762     },
4763 });
4764
4765 /**
4766  * Class to display a popup containing a form view.
4767  */
4768 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4769     show_element: function(model, row_id, context, options) {
4770         this.init_popup(model, row_id, [], context,  options);
4771         _.defaults(this.options, {
4772         });
4773         this.display_popup();
4774     },
4775     start: function() {
4776         this._super();
4777         this.init_dataset();
4778         this.setup_form_view();
4779     },
4780 });
4781
4782 /**
4783  * Class to display a popup to display a list to search a row. It also allows
4784  * to switch to a form view to create a new row.
4785  */
4786 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4787     /**
4788      * options:
4789      * - initial_ids
4790      * - initial_view: form or search (default search)
4791      * - disable_multiple_selection
4792      * - list_view_options
4793      */
4794     select_element: function(model, options, domain, context) {
4795         this.init_popup(model, null, domain, context, options);
4796         var self = this;
4797         _.defaults(this.options, {
4798             initial_view: "search",
4799         });
4800         this.initial_ids = this.options.initial_ids;
4801         this.display_popup();
4802     },
4803     start: function() {
4804         var self = this;
4805         this.init_dataset();
4806         if (this.options.initial_view == "search") {
4807             instance.web.pyeval.eval_domains_and_contexts({
4808                 domains: [],
4809                 contexts: [this.context]
4810             }).done(function (results) {
4811                 var search_defaults = {};
4812                 _.each(results.context, function (value_, key) {
4813                     var match = /^search_default_(.*)$/.exec(key);
4814                     if (match) {
4815                         search_defaults[match[1]] = value_;
4816                     }
4817                 });
4818                 self.setup_search_view(search_defaults);
4819             });
4820         } else { // "form"
4821             this.new_object();
4822         }
4823     },
4824     setup_search_view: function(search_defaults) {
4825         var self = this;
4826         if (this.searchview) {
4827             this.searchview.destroy();
4828         }
4829         this.searchview = new instance.web.SearchView(this,
4830                 this.dataset, false,  search_defaults);
4831         this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4832             if (self.initial_ids) {
4833                 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4834                     contexts.concat(self.context), groupbys);
4835                 self.initial_ids = undefined;
4836             } else {
4837                 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4838             }
4839         });
4840         this.searchview.on("search_view_loaded", self, function() {
4841             self.view_list = new instance.web.form.SelectCreateListView(self,
4842                     self.dataset, false,
4843                     _.extend({'deletable': false,
4844                         'selectable': !self.options.disable_multiple_selection,
4845                         'import_enabled': false,
4846                         '$buttons': self.$buttonpane,
4847                         'disable_editable_mode': true,
4848                         '$pager': self.$('.oe_popup_list_pager'),
4849                     }, self.options.list_view_options || {}));
4850             self.view_list.on('edit:before', self, function (e) {
4851                 e.cancel = true;
4852             });
4853             self.view_list.popup = self;
4854             self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4855                 self.view_list.do_show();
4856             }).then(function() {
4857                 self.searchview.do_search();
4858             });
4859             self.view_list.on("list_view_loaded", self, function() {
4860                 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4861                 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4862                 $cbutton.click(function() {
4863                     self.destroy();
4864                 });
4865                 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4866                 $sbutton.click(function() {
4867                     self.select_elements(self.selected_ids);
4868                     self.destroy();
4869                 });
4870                 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4871                 $cbutton.click(function() {
4872                     self.new_object();
4873                 });
4874             });
4875         });
4876         this.searchview.appendTo($(".oe_popup_search", self.$el));
4877     },
4878     do_search: function(domains, contexts, groupbys) {
4879         var self = this;
4880         instance.web.pyeval.eval_domains_and_contexts({
4881             domains: domains || [],
4882             contexts: contexts || [],
4883             group_by_seq: groupbys || []
4884         }).done(function (results) {
4885             self.view_list.do_search(results.domain, results.context, results.group_by);
4886         });
4887     },
4888     on_click_element: function(ids) {
4889         var self = this;
4890         this.selected_ids = ids || [];
4891         if(this.selected_ids.length > 0) {
4892             self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4893         } else {
4894             self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4895         }
4896     },
4897     new_object: function() {
4898         if (this.searchview) {
4899             this.searchview.hide();
4900         }
4901         if (this.view_list) {
4902             this.view_list.do_hide();
4903         }
4904         this.setup_form_view();
4905     },
4906 });
4907
4908 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4909     do_add_record: function () {
4910         this.popup.new_object();
4911     },
4912     select_record: function(index) {
4913         this.popup.select_elements([this.dataset.ids[index]]);
4914         this.popup.destroy();
4915     },
4916     do_select: function(ids, records) {
4917         this._super(ids, records);
4918         this.popup.on_click_element(ids);
4919     }
4920 });
4921
4922 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4923     template: 'FieldReference',
4924     init: function(field_manager, node) {
4925         this._super(field_manager, node);
4926         this.reference_ready = true;
4927     },
4928     destroy_content: function() {
4929         if (this.fm) {
4930             this.fm.destroy();
4931             this.fm = undefined;
4932         }
4933     },
4934     initialize_content: function() {
4935         var self = this;
4936         var fm = new instance.web.form.DefaultFieldManager(this);
4937         this.fm = fm;
4938         fm.extend_field_desc({
4939             "selection": {
4940                 selection: this.field_manager.get_field_desc(this.name).selection,
4941                 type: "selection",
4942             },
4943             "m2o": {
4944                 relation: null,
4945                 type: "many2one",
4946             },
4947         });
4948         this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4949             name: 'selection',
4950             modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4951         }});
4952         this.selection.on("change:value", this, this.on_selection_changed);
4953         this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4954         this.selection
4955             .on('focused', null, function () {self.trigger('focused')})
4956             .on('blurred', null, function () {self.trigger('blurred')});
4957
4958         this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4959             name: 'Referenced Document',
4960             modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4961         }});
4962         this.m2o.on("change:value", this, this.data_changed);
4963         this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4964         this.m2o
4965             .on('focused', null, function () {self.trigger('focused')})
4966             .on('blurred', null, function () {self.trigger('blurred')});
4967     },
4968     on_selection_changed: function() {
4969         if (this.reference_ready) {
4970             this.internal_set_value([this.selection.get_value(), false]);
4971             this.render_value();
4972         }
4973     },
4974     data_changed: function() {
4975         if (this.reference_ready) {
4976             this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4977         }
4978     },
4979     set_value: function(val) {
4980         if (val) {
4981             val = val.split(',');
4982             val[0] = val[0] || false;
4983             val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4984         }
4985         this._super(val || [false, false]);
4986     },
4987     get_value: function() {
4988         return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4989     },
4990     render_value: function() {
4991         this.reference_ready = false;
4992         if (!this.get("effective_readonly")) {
4993             this.selection.set_value(this.get('value')[0]);
4994         }
4995         this.m2o.field.relation = this.get('value')[0];
4996         this.m2o.set_value(this.get('value')[1]);
4997         this.m2o.$el.toggle(!!this.get('value')[0]);
4998         this.reference_ready = true;
4999     },
5000 });
5001
5002 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5003     init: function(field_manager, node) {
5004         var self = this;
5005         this._super(field_manager, node);
5006         this.binary_value = false;
5007         this.useFileAPI = !!window.FileReader;
5008         this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5009         if (!this.useFileAPI) {
5010             this.fileupload_id = _.uniqueId('oe_fileupload');
5011             $(window).on(this.fileupload_id, function() {
5012                 var args = [].slice.call(arguments).slice(1);
5013                 self.on_file_uploaded.apply(self, args);
5014             });
5015         }
5016     },
5017     stop: function() {
5018         if (!this.useFileAPI) {
5019             $(window).off(this.fileupload_id);
5020         }
5021         this._super.apply(this, arguments);
5022     },
5023     initialize_content: function() {
5024         this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5025         this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5026         this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5027     },
5028     on_file_change: function(e) {
5029         var self = this;
5030         var file_node = e.target;
5031         if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5032             if (this.useFileAPI) {
5033                 var file = file_node.files[0];
5034                 if (file.size > this.max_upload_size) {
5035                     var msg = _t("The selected file exceed the maximum file size of %s.");
5036                     instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5037                     return false;
5038                 }
5039                 var filereader = new FileReader();
5040                 filereader.readAsDataURL(file);
5041                 filereader.onloadend = function(upload) {
5042                     var data = upload.target.result;
5043                     data = data.split(',')[1];
5044                     self.on_file_uploaded(file.size, file.name, file.type, data);
5045                 };
5046             } else {
5047                 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5048                 this.$el.find('form.oe_form_binary_form').submit();
5049             }
5050             this.$el.find('.oe_form_binary_progress').show();
5051             this.$el.find('.oe_form_binary').hide();
5052         }
5053     },
5054     on_file_uploaded: function(size, name, content_type, file_base64) {
5055         if (size === false) {
5056             this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5057             // TODO: use openerp web crashmanager
5058             console.warn("Error while uploading file : ", name);
5059         } else {
5060             this.filename = name;
5061             this.on_file_uploaded_and_valid.apply(this, arguments);
5062         }
5063         this.$el.find('.oe_form_binary_progress').hide();
5064         this.$el.find('.oe_form_binary').show();
5065     },
5066     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5067     },
5068     on_save_as: function(ev) {
5069         var value = this.get('value');
5070         if (!value) {
5071             this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5072             ev.stopPropagation();
5073         } else {
5074             instance.web.blockUI();
5075             var c = instance.webclient.crashmanager;
5076             this.session.get_file({
5077                 url: '/web/binary/saveas_ajax',
5078                 data: {data: JSON.stringify({
5079                     model: this.view.dataset.model,
5080                     id: (this.view.datarecord.id || ''),
5081                     field: this.name,
5082                     filename_field: (this.node.attrs.filename || ''),
5083                     data: instance.web.form.is_bin_size(value) ? null : value,
5084                     context: this.view.dataset.get_context()
5085                 })},
5086                 complete: instance.web.unblockUI,
5087                 error: c.rpc_error.bind(c)
5088             });
5089             ev.stopPropagation();
5090             return false;
5091         }
5092     },
5093     set_filename: function(value) {
5094         var filename = this.node.attrs.filename;
5095         if (filename) {
5096             var tmp = {};
5097             tmp[filename] = value;
5098             this.field_manager.set_values(tmp);
5099         }
5100     },
5101     on_clear: function() {
5102         if (this.get('value') !== false) {
5103             this.binary_value = false;
5104             this.internal_set_value(false);
5105         }
5106         return false;
5107     }
5108 });
5109
5110 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5111     template: 'FieldBinaryFile',
5112     initialize_content: function() {
5113         this._super();
5114         if (this.get("effective_readonly")) {
5115             var self = this;
5116             this.$el.find('a').click(function(ev) {
5117                 if (self.get('value')) {
5118                     self.on_save_as(ev);
5119                 }
5120                 return false;
5121             });
5122         }
5123     },
5124     render_value: function() {
5125         if (!this.get("effective_readonly")) {
5126             var show_value;
5127             if (this.node.attrs.filename) {
5128                 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5129             } else {
5130                 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5131             }
5132             this.$el.find('input').eq(0).val(show_value);
5133         } else {
5134             this.$el.find('a').toggle(!!this.get('value'));
5135             if (this.get('value')) {
5136                 var show_value = _t("Download")
5137                 if (this.view)
5138                     show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5139                 this.$el.find('a').text(show_value);
5140             }
5141         }
5142     },
5143     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5144         this.binary_value = true;
5145         this.internal_set_value(file_base64);
5146         var show_value = name + " (" + instance.web.human_size(size) + ")";
5147         this.$el.find('input').eq(0).val(show_value);
5148         this.set_filename(name);
5149     },
5150     on_clear: function() {
5151         this._super.apply(this, arguments);
5152         this.$el.find('input').eq(0).val('');
5153         this.set_filename('');
5154     }
5155 });
5156
5157 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5158     template: 'FieldBinaryImage',
5159     placeholder: "/web/static/src/img/placeholder.png",
5160     render_value: function() {
5161         var self = this;
5162         var url;
5163         if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5164             url = 'data:image/png;base64,' + this.get('value');
5165         } else if (this.get('value')) {
5166             var id = JSON.stringify(this.view.datarecord.id || null);
5167             var field = this.name;
5168             if (this.options.preview_image)
5169                 field = this.options.preview_image;
5170             url = this.session.url('/web/binary/image', {
5171                                         model: this.view.dataset.model,
5172                                         id: id,
5173                                         field: field,
5174                                         t: (new Date().getTime()),
5175             });
5176         } else {
5177             url = this.placeholder;
5178         }
5179         var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5180         this.$el.find('> img').remove();
5181         this.$el.prepend($img);
5182         $img.load(function() {
5183             if (! self.options.size)
5184                 return;
5185             $img.css("max-width", "" + self.options.size[0] + "px");
5186             $img.css("max-height", "" + self.options.size[1] + "px");
5187             $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5188             $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5189         });
5190         $img.on('error', function() {
5191             $img.attr('src', self.placeholder);
5192             instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5193         });
5194     },
5195     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5196         this.internal_set_value(file_base64);
5197         this.binary_value = true;
5198         this.render_value();
5199         this.set_filename(name);
5200     },
5201     on_clear: function() {
5202         this._super.apply(this, arguments);
5203         this.render_value();
5204         this.set_filename('');
5205     },
5206     set_value: function(value_){
5207         var changed = value_ !== this.get_value();
5208         this._super.apply(this, arguments);
5209         // By default, on binary images read, the server returns the binary size
5210         // This is possible that two images have the exact same size
5211         // Therefore we trigger the change in case the image value hasn't changed
5212         // So the image is re-rendered correctly
5213         if (!changed){
5214             this.trigger("change:value", this, {
5215                 oldValue: value_,
5216                 newValue: value_
5217             });
5218         }
5219     }
5220 });
5221
5222 /**
5223  * Widget for (one2many field) to upload one or more file in same time and display in list.
5224  * The user can delete his files.
5225  * Options on attribute ; "blockui" {Boolean} block the UI or not
5226  * during the file is uploading
5227  */
5228 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5229     template: "FieldBinaryFileUploader",
5230     init: function(field_manager, node) {
5231         this._super(field_manager, node);
5232         this.field_manager = field_manager;
5233         this.node = node;
5234         if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5235             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);
5236         }
5237         this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5238         this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5239         $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5240     },
5241     start: function() {
5242         this._super(this);
5243         this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5244     },
5245     set_value: function(value_) {
5246         var value_ = value_ || [];
5247         var self = this;
5248         var ids = [];
5249         _.each(value_, function(command) {
5250             if (isNaN(command) && command.id == undefined) {
5251                 switch (command[0]) {
5252                     case commands.CREATE:
5253                         ids = ids.concat(command[2]);
5254                         return;
5255                     case commands.REPLACE_WITH:
5256                         ids = ids.concat(command[2]);
5257                         return;
5258                     case commands.UPDATE:
5259                         ids = ids.concat(command[2]);
5260                         return;
5261                     case commands.LINK_TO:
5262                         ids = ids.concat(command[1]);
5263                         return;
5264                     case commands.DELETE:
5265                         ids = _.filter(ids, function (id) { return id != command[1];});
5266                         return;
5267                     case commands.DELETE_ALL:
5268                         ids = [];
5269                         return;
5270                 }
5271             } else {
5272                 ids.push(command);
5273             }
5274         });
5275         this._super( ids );
5276     },
5277     get_value: function() {
5278         return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
5279     },
5280     get_file_url: function (attachment) {
5281         return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5282     },
5283     read_name_values : function () {
5284         var self = this;
5285         // select the list of id for a get_name
5286         var values = [];
5287         _.each(this.get('value'), function (val) {
5288             if (typeof val != 'object') {
5289                 values.push(val);
5290             }
5291         });
5292         // send request for get_name
5293         if (values.length) {
5294             return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
5295                 _.each(datas, function (data) {
5296                     data.no_unlink = true;
5297                     data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5298
5299                     _.each(self.get('value'), function (val, key) {
5300                         if(val == data.id) {
5301                             self.get('value')[key] = data;
5302                         }
5303                     });
5304                 });
5305             });
5306         } else {
5307             return $.when(this.get('value'));
5308         }
5309     },
5310     render_value: function () {
5311         var self = this;
5312         this.read_name_values().then(function (datas) {
5313
5314             var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5315             render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5316             self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5317
5318             // reinit input type file
5319             var $input = self.$('input.oe_form_binary_file');
5320             $input.after($input.clone(true)).remove();
5321             self.$(".oe_fileupload").show();
5322
5323         });
5324     },
5325     on_file_change: function (event) {
5326         event.stopPropagation();
5327         var self = this;
5328         var $target = $(event.target);
5329         if ($target.val() !== '') {
5330
5331             var filename = $target.val().replace(/.*[\\\/]/,'');
5332
5333             // if the files is currently uploded, don't send again
5334             if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
5335                 return false;
5336             }
5337
5338             // block UI or not
5339             if(this.node.attrs.blockui>0) {
5340                 instance.web.blockUI();
5341             }
5342
5343             // if the files exits for this answer, delete the file before upload
5344             var files = _.filter(this.get('value'), function (file) {
5345                 if((file.filename || file.name) == filename) {
5346                     self.ds_file.unlink([file.id]);
5347                     return false;
5348                 } else {
5349                     return true;
5350                 }
5351             });
5352
5353             // TODO : unactivate send on wizard and form
5354
5355             // submit file
5356             this.$('form.oe_form_binary_form').submit();
5357             this.$(".oe_fileupload").hide();
5358
5359             // add file on result
5360             files.push({
5361                 'id': 0,
5362                 'name': filename,
5363                 'filename': filename,
5364                 'url': '',
5365                 'upload': true
5366             });
5367
5368             this.set({'value': files});
5369         }
5370     },
5371     on_file_loaded: function (event, result) {
5372         var files = this.get('value');
5373
5374         // unblock UI
5375         if(this.node.attrs.blockui>0) {
5376             instance.web.unblockUI();
5377         }
5378
5379         // TODO : activate send on wizard and form
5380
5381         if (result.error || !result.id ) {
5382             this.do_warn( _t('Uploading Error'), result.error);
5383             files = _.filter(files, function (val) { return !val.upload; });
5384         } else {
5385             for(var i in files){
5386                 if(files[i].filename == result.filename && files[i].upload) {
5387                     files[i] = {
5388                         'id': result.id,
5389                         'name': result.name,
5390                         'filename': result.filename,
5391                         'url': this.get_file_url(result)
5392                     };
5393                 }
5394             }
5395         }
5396
5397         this.set({'value': files});
5398         this.render_value()
5399     },
5400     on_file_delete: function (event) {
5401         event.stopPropagation();
5402         var file_id=$(event.target).data("id");
5403         if (file_id) {
5404             var files=[];
5405             for(var i in this.get('value')){
5406                 if(file_id != this.get('value')[i].id){
5407                     files.push(this.get('value')[i]);
5408                 }
5409                 else if(!this.get('value')[i].no_unlink) {
5410                     this.ds_file.unlink([file_id]);
5411                 }
5412             }
5413             this.set({'value': files});
5414         }
5415     },
5416 });
5417
5418 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5419     template: "FieldStatus",
5420     init: function(field_manager, node) {
5421         this._super(field_manager, node);
5422         this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5423         this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5424         this.set({value: false});
5425         this.selection = [];
5426         this.set("selection", []);
5427         this.selection_dm = new instance.web.DropMisordered();
5428     },
5429     start: function() {
5430         this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5431         this.calc_domain();
5432         this.on("change:value", this, this.get_selection);
5433         this.on("change:evaluated_selection_domain", this, this.get_selection);
5434         this.on("change:selection", this, function() {
5435             this.selection = this.get("selection");
5436             this.render_value();
5437         });
5438         this.get_selection();
5439         if (this.options.clickable) {
5440             this.$el.on('click','li',this.on_click_stage);
5441         }
5442         this._super();
5443     },
5444     set_value: function(value_) {
5445         if (value_ instanceof Array) {
5446             value_ = value_[0];
5447         }
5448         this._super(value_);
5449     },
5450     render_value: function() {
5451         var self = this;
5452         var content = QWeb.render("FieldStatus.content", {widget: self});
5453         self.$el.html(content);
5454         var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5455         var color = colors[self.get('value')];
5456         if (color) {
5457             self.$("oe_active").css("color", color);
5458         }
5459     },
5460     calc_domain: function() {
5461         var d = instance.web.pyeval.eval('domain', this.build_domain());
5462         var domain = []; //if there is no domain defined, fetch all the records
5463         
5464         if (d.length) {
5465             domain = ['|',['id', '=', this.get('value')]].concat(d);
5466         }
5467         
5468         if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5469             this.set("evaluated_selection_domain", domain);
5470         }
5471     },
5472     /** Get the selection and render it
5473      *  selection: [[identifier, value_to_display], ...]
5474      *  For selection fields: this is directly given by this.field.selection
5475      *  For many2one fields:  perform a search on the relation of the many2one field
5476      */
5477     get_selection: function() {
5478         var self = this;
5479         var selection = [];
5480
5481         var calculation = _.bind(function() {
5482             if (this.field.type == "many2one") {
5483                 var domain = [];
5484                 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5485                     self.build_context(), this.get("evaluated_selection_domain"));
5486                 return ds.read_slice(['name'], {}).then(function (records) {
5487                     for(var i = 0; i < records.length; i++) {
5488                         selection.push([records[i].id, records[i].name]);
5489                     }
5490                 });
5491             } else {
5492                 // For field type selection filter values according to
5493                 // statusbar_visible attribute of the field. For example:
5494                 // statusbar_visible="draft,open".
5495                 var select = this.field.selection;
5496                 for(var i=0; i < select.length; i++) {
5497                     var key = select[i][0];
5498                     if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5499                         selection.push(select[i]);
5500                     }
5501                 }
5502                 return $.when();
5503             }
5504         }, this);
5505         this.selection_dm.add(calculation()).then(function () {
5506             if (! _.isEqual(selection, self.get("selection"))) {
5507                 self.set("selection", selection);
5508             }
5509         });
5510     },
5511     on_click_stage: function (ev) {
5512         var self = this;
5513         var $li = $(ev.currentTarget);
5514         if (this.field.type == "many2one") {
5515             var val = parseInt($li.data("id"));
5516         }
5517         else {
5518             var val = $li.data("id");
5519         }
5520         if (val != self.get('value')) {
5521             this.view.recursive_save().done(function() {
5522                 var change = {};
5523                 change[self.name] = val;
5524                 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5525                     self.view.reload();
5526                 });
5527             });
5528         }
5529     },
5530 });
5531
5532 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5533     template: "FieldMonetary",
5534     widget_class: 'oe_form_field_float oe_form_field_monetary',
5535     init: function() {
5536         this._super.apply(this, arguments);
5537         this.set({"currency": false});
5538         if (this.options.currency_field) {
5539             this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5540                 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5541             });
5542         }
5543         this.on("change:currency", this, this.get_currency_info);
5544         this.get_currency_info();
5545         this.ci_dm = new instance.web.DropMisordered();
5546     },
5547     start: function() {
5548         var tmp = this._super();
5549         this.on("change:currency_info", this, this.reinitialize);
5550         return tmp;
5551     },
5552     get_currency_info: function() {
5553         var self = this;
5554         if (this.get("currency") === false) {
5555             this.set({"currency_info": null});
5556             return;
5557         }
5558         return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5559             .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5560             self.set({"currency_info": res});
5561         });
5562     },
5563     parse_value: function(val, def) {
5564         return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5565     },
5566     format_value: function(val, def) {
5567         return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5568     },
5569 });
5570
5571 /**
5572  * Registry of form fields, called by :js:`instance.web.FormView`.
5573  *
5574  * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5575  * will substitute to the <field> tags as defined in OpenERP's views.
5576  */
5577 instance.web.form.widgets = new instance.web.Registry({
5578     'char' : 'instance.web.form.FieldChar',
5579     'id' : 'instance.web.form.FieldID',
5580     'email' : 'instance.web.form.FieldEmail',
5581     'url' : 'instance.web.form.FieldUrl',
5582     'text' : 'instance.web.form.FieldText',
5583     'html' : 'instance.web.form.FieldTextHtml',
5584     'date' : 'instance.web.form.FieldDate',
5585     'datetime' : 'instance.web.form.FieldDatetime',
5586     'selection' : 'instance.web.form.FieldSelection',
5587     'many2one' : 'instance.web.form.FieldMany2One',
5588     'many2onebutton' : 'instance.web.form.Many2OneButton',
5589     'many2many' : 'instance.web.form.FieldMany2Many',
5590     'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5591     'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5592     'one2many' : 'instance.web.form.FieldOne2Many',
5593     'one2many_list' : 'instance.web.form.FieldOne2Many',
5594     'reference' : 'instance.web.form.FieldReference',
5595     'boolean' : 'instance.web.form.FieldBoolean',
5596     'float' : 'instance.web.form.FieldFloat',
5597     'integer': 'instance.web.form.FieldFloat',
5598     'float_time': 'instance.web.form.FieldFloat',
5599     'progressbar': 'instance.web.form.FieldProgressBar',
5600     'image': 'instance.web.form.FieldBinaryImage',
5601     'binary': 'instance.web.form.FieldBinaryFile',
5602     'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5603     'statusbar': 'instance.web.form.FieldStatus',
5604     'monetary': 'instance.web.form.FieldMonetary',
5605 });
5606
5607 /**
5608  * Registry of widgets usable in the form view that can substitute to any possible
5609  * tags defined in OpenERP's form views.
5610  *
5611  * Every referenced class should extend FormWidget.
5612  */
5613 instance.web.form.tags = new instance.web.Registry({
5614     'button' : 'instance.web.form.WidgetButton',
5615 });
5616
5617 instance.web.form.custom_widgets = new instance.web.Registry({
5618 });
5619
5620 };
5621
5622 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: