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