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         this.o2m._dirty_flag = true;
3898         var r;
3899         return _.every(this.records.records, function(record){
3900             r = record;
3901             _.each(self.editor.form.fields, function(field){
3902                 field._inhibit_on_change_flag = true;
3903                 field.set_value(r.attributes[field.name]);
3904                 field._inhibit_on_change_flag = false;
3905             });
3906             return _.every(self.editor.form.fields, function(field){
3907                 field.process_modifiers();
3908                 field._check_css_flags();
3909                 return field.is_valid();
3910             });
3911         });
3912     },
3913     do_add_record: function () {
3914         if (this.editable()) {
3915             this._super.apply(this, arguments);
3916         } else {
3917             var self = this;
3918             var pop = new instance.web.form.SelectCreatePopup(this);
3919             pop.select_element(
3920                 self.o2m.field.relation,
3921                 {
3922                     title: _t("Create: ") + self.o2m.string,
3923                     initial_view: "form",
3924                     alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3925                     create_function: function(data, options) {
3926                         return self.o2m.dataset.create(data, options).done(function(r) {
3927                             self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3928                             self.o2m.dataset.trigger("dataset_changed", r);
3929                         });
3930                     },
3931                     read_function: function() {
3932                         return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3933                     },
3934                     parent_view: self.o2m.view,
3935                     child_name: self.o2m.name,
3936                     form_view_options: {'not_interactible_on_create':true}
3937                 },
3938                 self.o2m.build_domain(),
3939                 self.o2m.build_context()
3940             );
3941             pop.on("elements_selected", self, function() {
3942                 self.o2m.reload_current_view();
3943             });
3944         }
3945     },
3946     do_activate_record: function(index, id) {
3947         var self = this;
3948         var pop = new instance.web.form.FormOpenPopup(self);
3949         pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3950             title: _t("Open: ") + self.o2m.string,
3951             write_function: function(id, data) {
3952                 return self.o2m.dataset.write(id, data, {}).done(function() {
3953                     self.o2m.reload_current_view();
3954                 });
3955             },
3956             alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3957             parent_view: self.o2m.view,
3958             child_name: self.o2m.name,
3959             read_function: function() {
3960                 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3961             },
3962             form_view_options: {'not_interactible_on_create':true},
3963             readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
3964         });
3965     },
3966     do_button_action: function (name, id, callback) {
3967         if (!_.isNumber(id)) {
3968             instance.webclient.notification.warn(
3969                 _t("Action Button"),
3970                 _t("The o2m record must be saved before an action can be used"));
3971             return;
3972         }
3973         var parent_form = this.o2m.view;
3974         var self = this;
3975         this.ensure_saved().then(function () {
3976             if (parent_form)
3977                 return parent_form.save();
3978             else
3979                 return $.when();
3980         }).done(function () {
3981             var ds = self.o2m.dataset;
3982             var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
3983                 return value.length;
3984             });
3985             if (!self.o2m.options.reload_on_button && !cached_records) {
3986                 self.handle_button(name, id, callback);
3987             }else {
3988                 self.handle_button(name, id, function(){
3989                     self.o2m.view.reload();
3990                 });
3991             }
3992         });
3993     },
3994
3995     _after_edit: function () {
3996         this.__ignore_blur = false;
3997         this.editor.form.on('blurred', this, this._on_form_blur);
3998
3999         // The form's blur thing may be jiggered during the edition setup,
4000         // potentially leading to the o2m instasaving the row. Cancel any
4001         // blurring triggered the edition startup here
4002         this.editor.form.widgetFocused();
4003     },
4004     _before_unedit: function () {
4005         this.editor.form.off('blurred', this, this._on_form_blur);
4006     },
4007     _button_down: function () {
4008         // If a button is clicked (usually some sort of action button), it's
4009         // the button's responsibility to ensure the editable list is in the
4010         // correct state -> ignore form blurring
4011         this.__ignore_blur = true;
4012     },
4013     /**
4014      * Handles blurring of the nested form (saves the currently edited row),
4015      * unless the flag to ignore the event is set to ``true``
4016      *
4017      * Makes the internal form go away
4018      */
4019     _on_form_blur: function () {
4020         if (this.__ignore_blur) {
4021             this.__ignore_blur = false;
4022             return;
4023         }
4024         // FIXME: why isn't there an API for this?
4025         if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4026             this.ensure_saved();
4027             return;
4028         }
4029         this.cancel_edition();
4030     },
4031     keypress_ENTER: function () {
4032         // blurring caused by hitting the [Return] key, should skip the
4033         // autosave-on-blur and let the handler for [Return] do its thing (save
4034         // the current row *anyway*, then create a new one/edit the next one)
4035         this.__ignore_blur = true;
4036         this._super.apply(this, arguments);
4037     },
4038     do_delete: function (ids) {
4039         var confirm = window.confirm;
4040         window.confirm = function () { return true; };
4041         try {
4042             return this._super(ids);
4043         } finally {
4044             window.confirm = confirm;
4045         }
4046     },
4047     reload_record: function (record) {
4048         // Evict record.id from cache to ensure it will be reloaded correctly
4049         this.dataset.evict_record(record.get('id'));
4050
4051         return this._super(record);
4052     }
4053 });
4054 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4055     setup_resequence_rows: function () {
4056         if (!this.view.o2m.get('effective_readonly')) {
4057             this._super.apply(this, arguments);
4058         }
4059     }
4060 });
4061 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4062     pad_table_to: function (count) {
4063         if (!this.view.is_action_enabled('create')) {
4064             this._super(count);
4065         } else {
4066             this._super(count > 0 ? count - 1 : 0);
4067         }
4068
4069         // magical invocation of wtf does that do
4070         if (this.view.o2m.get('effective_readonly')) {
4071             return;
4072         }
4073
4074         var self = this;
4075         var columns = _(this.columns).filter(function (column) {
4076             return column.invisible !== '1';
4077         }).length;
4078         if (this.options.selectable) { columns++; }
4079         if (this.options.deletable) { columns++; }
4080
4081         if (!this.view.is_action_enabled('create')) {
4082             return;
4083         }
4084
4085         var $cell = $('<td>', {
4086             colspan: columns,
4087             'class': 'oe_form_field_one2many_list_row_add'
4088         }).append(
4089             $('<a>', {href: '#'}).text(_t("Add an item"))
4090                 .mousedown(function () {
4091                     // FIXME: needs to be an official API somehow
4092                     if (self.view.editor.is_editing()) {
4093                         self.view.__ignore_blur = true;
4094                     }
4095                 })
4096                 .click(function (e) {
4097                     e.preventDefault();
4098                     e.stopPropagation();
4099                     // FIXME: there should also be an API for that one
4100                     if (self.view.editor.form.__blur_timeout) {
4101                         clearTimeout(self.view.editor.form.__blur_timeout);
4102                         self.view.editor.form.__blur_timeout = false;
4103                     }
4104                     self.view.ensure_saved().done(function () {
4105                         self.view.do_add_record();
4106                     });
4107                 }));
4108
4109         var $padding = this.$current.find('tr:not([data-id]):first');
4110         var $newrow = $('<tr>').append($cell);
4111         if ($padding.length) {
4112             $padding.before($newrow);
4113         } else {
4114             this.$current.append($newrow)
4115         }
4116     }
4117 });
4118
4119 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4120     form_template: 'One2Many.formview',
4121     load_form: function(data) {
4122         this._super(data);
4123         var self = this;
4124         this.$buttons.find('button.oe_form_button_create').click(function() {
4125             self.save().done(self.on_button_new);
4126         });
4127     },
4128     do_notify_change: function() {
4129         if (this.dataset.parent_view) {
4130             this.dataset.parent_view.do_notify_change();
4131         } else {
4132             this._super.apply(this, arguments);
4133         }
4134     }
4135 });
4136
4137 var lazy_build_o2m_kanban_view = function() {
4138     if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4139         return;
4140     instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4141     });
4142 };
4143
4144 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4145     template: "FieldMany2ManyTags",
4146     init: function() {
4147         this._super.apply(this, arguments);
4148         instance.web.form.CompletionFieldMixin.init.call(this);
4149         this.set({"value": []});
4150         this._display_orderer = new instance.web.DropMisordered();
4151         this._drop_shown = false;
4152     },
4153     initialize_content: function() {
4154         if (this.get("effective_readonly"))
4155             return;
4156         var self = this;
4157         var ignore_blur = false;
4158         self.$text = this.$("textarea");
4159         self.$text.textext({
4160             plugins : 'tags arrow autocomplete',
4161             autocomplete: {
4162                 render: function(suggestion) {
4163                     return $('<span class="text-label"/>').
4164                              data('index', suggestion['index']).html(suggestion['label']);
4165                 }
4166             },
4167             ext: {
4168                 autocomplete: {
4169                     selectFromDropdown: function() {
4170                         this.trigger('hideDropdown');
4171                         var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4172                         var data = self.search_result[index];
4173                         if (data.id) {
4174                             self.add_id(data.id);
4175                         } else {
4176                             ignore_blur = true;
4177                             data.action();
4178                         }
4179                         this.trigger('setSuggestions', {result : []});
4180                     },
4181                 },
4182                 tags: {
4183                     isTagAllowed: function(tag) {
4184                         return !!tag.name;
4185
4186                     },
4187                     removeTag: function(tag) {
4188                         var id = tag.data("id");
4189                         self.set({"value": _.without(self.get("value"), id)});
4190                     },
4191                     renderTag: function(stuff) {
4192                         return $.fn.textext.TextExtTags.prototype.renderTag.
4193                             call(this, stuff).data("id", stuff.id);
4194                     },
4195                 },
4196                 itemManager: {
4197                     itemToString: function(item) {
4198                         return item.name;
4199                     },
4200                 },
4201                 core: {
4202                     onSetInputData: function(e, data) {
4203                         if (data == '') {
4204                             this._plugins.autocomplete._suggestions = null;
4205                         }
4206                         this.input().val(data);
4207                     },
4208                 },
4209             },
4210         }).bind('getSuggestions', function(e, data) {
4211             var _this = this;
4212             var str = !!data ? data.query || '' : '';
4213             self.get_search_result(str).done(function(result) {
4214                 self.search_result = result;
4215                 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4216                     return _.extend(el, {index:i});
4217                 })});
4218             });
4219         }).bind('hideDropdown', function() {
4220             self._drop_shown = false;
4221         }).bind('showDropdown', function() {
4222             self._drop_shown = true;
4223         });
4224         self.tags = self.$text.textext()[0].tags();
4225         self.$text
4226             .focusin(function () {
4227                 self.trigger('focused');
4228                 ignore_blur = false;
4229             })
4230             .focusout(function() {
4231                 self.$text.trigger("setInputData", "");
4232                 if (!ignore_blur) {
4233                     self.trigger('blurred');
4234                 }
4235             }).keydown(function(e) {
4236                 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4237                     self.$text.textext()[0].autocomplete().selectFromDropdown();
4238                 }
4239             });
4240     },
4241     set_value: function(value_) {
4242         value_ = value_ || [];
4243         if (value_.length >= 1 && value_[0] instanceof Array) {
4244             value_ = value_[0][2];
4245         }
4246         this._super(value_);
4247     },
4248     is_false: function() {
4249         return _(this.get("value")).isEmpty();
4250     },
4251     get_value: function() {
4252         var tmp = [commands.replace_with(this.get("value"))];
4253         return tmp;
4254     },
4255     get_search_blacklist: function() {
4256         return this.get("value");
4257     },
4258     render_value: function() {
4259         var self = this;
4260         var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4261         var values = self.get("value");
4262         var handle_names = function(data) {
4263             if (self.isDestroyed())
4264                 return;
4265             var indexed = {};
4266             _.each(data, function(el) {
4267                 indexed[el[0]] = el;
4268             });
4269             data = _.map(values, function(el) { return indexed[el]; });
4270             if (! self.get("effective_readonly")) {
4271                 self.tags.containerElement().children().remove();
4272                 self.$('textarea').css("padding-left", "3px");
4273                 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4274             } else {
4275                 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4276             }
4277         };
4278         if (! values || values.length > 0) {
4279             return this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4280         } else {
4281             handle_names([]);
4282         }
4283     },
4284     add_id: function(id) {
4285         this.set({'value': _.uniq(this.get('value').concat([id]))});
4286     },
4287     focus: function () {
4288         var input = this.$text && this.$text[0];
4289         return input ? input.focus() : false;
4290     },
4291 });
4292
4293 /**
4294     widget options:
4295     - reload_on_button: Reload the whole form view if click on a button in a list view.
4296         If you see this options, do not use it, it's basically a dirty hack to make one
4297         precise o2m to behave the way we want.
4298 */
4299 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4300     multi_selection: false,
4301     init: function(field_manager, node) {
4302         this._super(field_manager, node);
4303         this.is_loaded = $.Deferred();
4304         this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4305         this.dataset.m2m = this;
4306         var self = this;
4307         this.dataset.on('unlink', self, function(ids) {
4308             self.dataset_changed();
4309         });
4310         this.set_value([]);
4311         this.list_dm = new instance.web.DropMisordered();
4312         this.render_value_dm = new instance.web.DropMisordered();
4313     },
4314     initialize_content: function() {
4315         var self = this;
4316
4317         this.$el.addClass('oe_form_field oe_form_field_many2many');
4318
4319         this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4320                     'addable': this.get("effective_readonly") ? null : _t("Add"),
4321                     'deletable': this.get("effective_readonly") ? false : true,
4322                     'selectable': this.multi_selection,
4323                     'sortable': false,
4324                     'reorderable': false,
4325                     'import_enabled': false,
4326             });
4327         var embedded = (this.field.views || {}).tree;
4328         if (embedded) {
4329             this.list_view.set_embedded_view(embedded);
4330         }
4331         this.list_view.m2m_field = this;
4332         var loaded = $.Deferred();
4333         this.list_view.on("list_view_loaded", this, function() {
4334             loaded.resolve();
4335         });
4336         this.list_view.appendTo(this.$el);
4337
4338         var old_def = self.is_loaded;
4339         self.is_loaded = $.Deferred().done(function() {
4340             old_def.resolve();
4341         });
4342         this.list_dm.add(loaded).then(function() {
4343             self.is_loaded.resolve();
4344         });
4345     },
4346     destroy_content: function() {
4347         this.list_view.destroy();
4348         this.list_view = undefined;
4349     },
4350     set_value: function(value_) {
4351         value_ = value_ || [];
4352         if (value_.length >= 1 && value_[0] instanceof Array) {
4353             value_ = value_[0][2];
4354         }
4355         this._super(value_);
4356     },
4357     get_value: function() {
4358         return [commands.replace_with(this.get('value'))];
4359     },
4360     is_false: function () {
4361         return _(this.get("value")).isEmpty();
4362     },
4363     render_value: function() {
4364         var self = this;
4365         this.dataset.set_ids(this.get("value"));
4366         this.render_value_dm.add(this.is_loaded).then(function() {
4367             return self.list_view.reload_content();
4368         });
4369     },
4370     dataset_changed: function() {
4371         this.internal_set_value(this.dataset.ids);
4372     },
4373 });
4374
4375 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4376     get_context: function() {
4377         this.context = this.m2m.build_context();
4378         return this.context;
4379     }
4380 });
4381
4382 /**
4383  * @class
4384  * @extends instance.web.ListView
4385  */
4386 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4387     do_add_record: function () {
4388         var pop = new instance.web.form.SelectCreatePopup(this);
4389         pop.select_element(
4390             this.model,
4391             {
4392                 title: _t("Add: ") + this.m2m_field.string,
4393                 no_create: this.m2m_field.options.no_create,
4394             },
4395             new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4396             this.m2m_field.build_context()
4397         );
4398         var self = this;
4399         pop.on("elements_selected", self, function(element_ids) {
4400             var reload = false;
4401             _(element_ids).each(function (id) {
4402                 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4403                     self.dataset.set_ids(self.dataset.ids.concat([id]));
4404                     self.m2m_field.dataset_changed();
4405                     reload = true;
4406                 }
4407             });
4408             if (reload) {
4409                 self.reload_content();
4410             }
4411         });
4412     },
4413     do_activate_record: function(index, id) {
4414         var self = this;
4415         var pop = new instance.web.form.FormOpenPopup(this);
4416         pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4417             title: _t("Open: ") + this.m2m_field.string,
4418             readonly: this.getParent().get("effective_readonly")
4419         });
4420         pop.on('write_completed', self, self.reload_content);
4421     },
4422     do_button_action: function(name, id, callback) {
4423         var self = this;
4424         var _sup = _.bind(this._super, this);
4425         if (! this.m2m_field.options.reload_on_button) {
4426             return _sup(name, id, callback);
4427         } else {
4428             return this.m2m_field.view.save().then(function() {
4429                 return _sup(name, id, function() {
4430                     self.m2m_field.view.reload();
4431                 });
4432             });
4433         }
4434      },
4435     is_action_enabled: function () { return true; },
4436 });
4437
4438 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4439     disable_utility_classes: true,
4440     init: function(field_manager, node) {
4441         this._super(field_manager, node);
4442         instance.web.form.CompletionFieldMixin.init.call(this);
4443         m2m_kanban_lazy_init();
4444         this.is_loaded = $.Deferred();
4445         this.initial_is_loaded = this.is_loaded;
4446
4447         var self = this;
4448         this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4449         this.dataset.m2m = this;
4450         this.dataset.on('unlink', self, function(ids) {
4451             self.dataset_changed();
4452         });
4453     },
4454     start: function() {
4455         this._super.apply(this, arguments);
4456
4457         var self = this;
4458
4459         self.load_view();
4460         self.on("change:effective_readonly", self, function() {
4461             self.is_loaded = self.is_loaded.then(function() {
4462                 self.kanban_view.destroy();
4463                 return $.when(self.load_view()).done(function() {
4464                     self.render_value();
4465                 });
4466             });
4467         });
4468     },
4469     set_value: function(value_) {
4470         value_ = value_ || [];
4471         if (value_.length >= 1 && value_[0] instanceof Array) {
4472             value_ = value_[0][2];
4473         }
4474         this._super(value_);
4475     },
4476     get_value: function() {
4477         return [commands.replace_with(this.get('value'))];
4478     },
4479     load_view: function() {
4480         var self = this;
4481         this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4482                     'create_text': _t("Add"),
4483                     'creatable': self.get("effective_readonly") ? false : true,
4484                     'quick_creatable': self.get("effective_readonly") ? false : true,
4485                     'read_only_mode': self.get("effective_readonly") ? true : false,
4486                     'confirm_on_delete': false,
4487             });
4488         var embedded = (this.field.views || {}).kanban;
4489         if (embedded) {
4490             this.kanban_view.set_embedded_view(embedded);
4491         }
4492         this.kanban_view.m2m = this;
4493         var loaded = $.Deferred();
4494         this.kanban_view.on("kanban_view_loaded",self,function() {
4495             self.initial_is_loaded.resolve();
4496             loaded.resolve();
4497         });
4498         this.kanban_view.on('switch_mode', this, this.open_popup);
4499         $.async_when().done(function () {
4500             self.kanban_view.appendTo(self.$el);
4501         });
4502         return loaded;
4503     },
4504     render_value: function() {
4505         var self = this;
4506         this.dataset.set_ids(this.get("value"));
4507         this.is_loaded = this.is_loaded.then(function() {
4508             return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4509         });
4510     },
4511     dataset_changed: function() {
4512         this.set({'value': this.dataset.ids});
4513     },
4514     open_popup: function(type, unused) {
4515         if (type !== "form")
4516             return;
4517         var self = this;
4518         if (this.dataset.index === null) {
4519             var pop = new instance.web.form.SelectCreatePopup(this);
4520             pop.select_element(
4521                 this.field.relation,
4522                 {
4523                     title: _t("Add: ") + this.string
4524                 },
4525                 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4526                 this.build_context()
4527             );
4528             pop.on("elements_selected", self, function(element_ids) {
4529                 _.each(element_ids, function(one_id) {
4530                     if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4531                         self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4532                         self.dataset_changed();
4533                         self.render_value();
4534                     }
4535                 });
4536             });
4537         } else {
4538             var id = self.dataset.ids[self.dataset.index];
4539             var pop = new instance.web.form.FormOpenPopup(this);
4540             pop.show_element(self.field.relation, id, self.build_context(), {
4541                 title: _t("Open: ") + self.string,
4542                 write_function: function(id, data, options) {
4543                     return self.dataset.write(id, data, {}).done(function() {
4544                         self.render_value();
4545                     });
4546                 },
4547                 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4548                 parent_view: self.view,
4549                 child_name: self.name,
4550                 readonly: self.get("effective_readonly")
4551             });
4552         }
4553     },
4554     add_id: function(id) {
4555         this.quick_create.add_id(id);
4556     },
4557 });
4558
4559 function m2m_kanban_lazy_init() {
4560 if (instance.web.form.Many2ManyKanbanView)
4561     return;
4562 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4563     quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4564     _is_quick_create_enabled: function() {
4565         return this._super() && ! this.group_by;
4566     },
4567 });
4568 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4569     template: 'Many2ManyKanban.quick_create',
4570
4571     /**
4572      * close_btn: If true, the widget will display a "Close" button able to trigger
4573      * a "close" event.
4574      */
4575     init: function(parent, dataset, context, buttons) {
4576         this._super(parent);
4577         this.m2m = this.getParent().view.m2m;
4578         this.m2m.quick_create = this;
4579         this._dataset = dataset;
4580         this._buttons = buttons || false;
4581         this._context = context || {};
4582     },
4583     start: function () {
4584         var self = this;
4585         self.$text = this.$el.find('input').css("width", "200px");
4586         self.$text.textext({
4587             plugins : 'arrow autocomplete',
4588             autocomplete: {
4589                 render: function(suggestion) {
4590                     return $('<span class="text-label"/>').
4591                              data('index', suggestion['index']).html(suggestion['label']);
4592                 }
4593             },
4594             ext: {
4595                 autocomplete: {
4596                     selectFromDropdown: function() {
4597                         $(this).trigger('hideDropdown');
4598                         var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4599                         var data = self.search_result[index];
4600                         if (data.id) {
4601                             self.add_id(data.id);
4602                         } else {
4603                             data.action();
4604                         }
4605                     },
4606                 },
4607                 itemManager: {
4608                     itemToString: function(item) {
4609                         return item.name;
4610                     },
4611                 },
4612             },
4613         }).bind('getSuggestions', function(e, data) {
4614             var _this = this;
4615             var str = !!data ? data.query || '' : '';
4616             self.m2m.get_search_result(str).done(function(result) {
4617                 self.search_result = result;
4618                 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4619                     return _.extend(el, {index:i});
4620                 })});
4621             });
4622         });
4623         self.$text.focusout(function() {
4624             self.$text.val("");
4625         });
4626     },
4627     focus: function() {
4628         this.$text[0].focus();
4629     },
4630     add_id: function(id) {
4631         var self = this;
4632         self.$text.val("");
4633         self.trigger('added', id);
4634         this.m2m.dataset_changed();
4635     },
4636 });
4637 }
4638
4639 /**
4640  * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4641  */
4642 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4643     template: "AbstractFormPopup.render",
4644     /**
4645      *  options:
4646      *  -readonly: only applicable when not in creation mode, default to false
4647      * - alternative_form_view
4648      * - view_id
4649      * - write_function
4650      * - read_function
4651      * - create_function
4652      * - parent_view
4653      * - child_name
4654      * - form_view_options
4655      */
4656     init_popup: function(model, row_id, domain, context, options) {
4657         this.row_id = row_id;
4658         this.model = model;
4659         this.domain = domain || [];
4660         this.context = context || {};
4661         this.options = options;
4662         _.defaults(this.options, {
4663         });
4664     },
4665     init_dataset: function() {
4666         var self = this;
4667         this.created_elements = [];
4668         this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4669         this.dataset.read_function = this.options.read_function;
4670         this.dataset.create_function = function(data, options, sup) {
4671             var fct = self.options.create_function || sup;
4672             return fct.call(this, data, options).done(function(r) {
4673                 self.trigger('create_completed saved', r);
4674                 self.created_elements.push(r);
4675             });
4676         };
4677         this.dataset.write_function = function(id, data, options, sup) {
4678             var fct = self.options.write_function || sup;
4679             return fct.call(this, id, data, options).done(function(r) {
4680                 self.trigger('write_completed saved', r);
4681             });
4682         };
4683         this.dataset.parent_view = this.options.parent_view;
4684         this.dataset.child_name = this.options.child_name;
4685     },
4686     display_popup: function() {
4687         var self = this;
4688         this.renderElement();
4689         var dialog = new instance.web.Dialog(this, {
4690             min_width: '800px',
4691             dialogClass: 'oe_act_window',
4692             close: function() {
4693                 self.check_exit(true);
4694             },
4695             title: this.options.title || "",
4696         }, this.$el).open();
4697         this.$buttonpane = dialog.$buttons;
4698         this.start();
4699     },
4700     setup_form_view: function() {
4701         var self = this;
4702         if (this.row_id) {
4703             this.dataset.ids = [this.row_id];
4704             this.dataset.index = 0;
4705         } else {
4706             this.dataset.index = null;
4707         }
4708         var options = _.clone(self.options.form_view_options) || {};
4709         if (this.row_id !== null) {
4710             options.initial_mode = this.options.readonly ? "view" : "edit";
4711         }
4712         _.extend(options, {
4713             $buttons: this.$buttonpane,
4714         });
4715         this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4716         if (this.options.alternative_form_view) {
4717             this.view_form.set_embedded_view(this.options.alternative_form_view);
4718         }
4719         this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4720         this.view_form.on("form_view_loaded", self, function() {
4721             var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4722             self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4723                 multi_select: multi_select,
4724                 readonly: self.row_id !== null && self.options.readonly,
4725             }));
4726             var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4727             $snbutton.click(function() {
4728                 $.when(self.view_form.save()).done(function() {
4729                     self.view_form.reload_mutex.exec(function() {
4730                         self.view_form.on_button_new();
4731                     });
4732                 });
4733             });
4734             var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4735             $sbutton.click(function() {
4736                 $.when(self.view_form.save()).done(function() {
4737                     self.view_form.reload_mutex.exec(function() {
4738                         self.check_exit();
4739                     });
4740                 });
4741             });
4742             var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4743             $cbutton.click(function() {
4744                 self.view_form.trigger('on_button_cancel');
4745                 self.check_exit();
4746             });
4747             self.view_form.do_show();
4748         });
4749     },
4750     select_elements: function(element_ids) {
4751         this.trigger("elements_selected", element_ids);
4752     },
4753     check_exit: function(no_destroy) {
4754         if (this.created_elements.length > 0) {
4755             this.select_elements(this.created_elements);
4756             this.created_elements = [];
4757         }
4758         this.trigger('closed');
4759         this.destroy();
4760     },
4761     destroy: function () {
4762         this.trigger('closed');
4763         if (this.$el.is(":data(dialog)")) {
4764             this.$el.dialog('close');
4765         }
4766         this._super();
4767     },
4768 });
4769
4770 /**
4771  * Class to display a popup containing a form view.
4772  */
4773 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4774     show_element: function(model, row_id, context, options) {
4775         this.init_popup(model, row_id, [], context,  options);
4776         _.defaults(this.options, {
4777         });
4778         this.display_popup();
4779     },
4780     start: function() {
4781         this._super();
4782         this.init_dataset();
4783         this.setup_form_view();
4784     },
4785 });
4786
4787 /**
4788  * Class to display a popup to display a list to search a row. It also allows
4789  * to switch to a form view to create a new row.
4790  */
4791 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4792     /**
4793      * options:
4794      * - initial_ids
4795      * - initial_view: form or search (default search)
4796      * - disable_multiple_selection
4797      * - list_view_options
4798      */
4799     select_element: function(model, options, domain, context) {
4800         this.init_popup(model, null, domain, context, options);
4801         var self = this;
4802         _.defaults(this.options, {
4803             initial_view: "search",
4804         });
4805         this.initial_ids = this.options.initial_ids;
4806         this.display_popup();
4807     },
4808     start: function() {
4809         var self = this;
4810         this.init_dataset();
4811         if (this.options.initial_view == "search") {
4812             instance.web.pyeval.eval_domains_and_contexts({
4813                 domains: [],
4814                 contexts: [this.context]
4815             }).done(function (results) {
4816                 var search_defaults = {};
4817                 _.each(results.context, function (value_, key) {
4818                     var match = /^search_default_(.*)$/.exec(key);
4819                     if (match) {
4820                         search_defaults[match[1]] = value_;
4821                     }
4822                 });
4823                 self.setup_search_view(search_defaults);
4824             });
4825         } else { // "form"
4826             this.new_object();
4827         }
4828     },
4829     setup_search_view: function(search_defaults) {
4830         var self = this;
4831         if (this.searchview) {
4832             this.searchview.destroy();
4833         }
4834         this.searchview = new instance.web.SearchView(this,
4835                 this.dataset, false,  search_defaults);
4836         this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4837             if (self.initial_ids) {
4838                 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4839                     contexts.concat(self.context), groupbys);
4840                 self.initial_ids = undefined;
4841             } else {
4842                 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4843             }
4844         });
4845         this.searchview.on("search_view_loaded", self, function() {
4846             self.view_list = new instance.web.form.SelectCreateListView(self,
4847                     self.dataset, false,
4848                     _.extend({'deletable': false,
4849                         'selectable': !self.options.disable_multiple_selection,
4850                         'import_enabled': false,
4851                         '$buttons': self.$buttonpane,
4852                         'disable_editable_mode': true,
4853                         '$pager': self.$('.oe_popup_list_pager'),
4854                     }, self.options.list_view_options || {}));
4855             self.view_list.on('edit:before', self, function (e) {
4856                 e.cancel = true;
4857             });
4858             self.view_list.popup = self;
4859             self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4860                 self.view_list.do_show();
4861             }).then(function() {
4862                 self.searchview.do_search();
4863             });
4864             self.view_list.on("list_view_loaded", self, function() {
4865                 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4866                 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4867                 $cbutton.click(function() {
4868                     self.destroy();
4869                 });
4870                 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4871                 $sbutton.click(function() {
4872                     self.select_elements(self.selected_ids);
4873                     self.destroy();
4874                 });
4875                 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4876                 $cbutton.click(function() {
4877                     self.new_object();
4878                 });
4879             });
4880         });
4881         this.searchview.appendTo($(".oe_popup_search", self.$el));
4882     },
4883     do_search: function(domains, contexts, groupbys) {
4884         var self = this;
4885         instance.web.pyeval.eval_domains_and_contexts({
4886             domains: domains || [],
4887             contexts: contexts || [],
4888             group_by_seq: groupbys || []
4889         }).done(function (results) {
4890             self.view_list.do_search(results.domain, results.context, results.group_by);
4891         });
4892     },
4893     on_click_element: function(ids) {
4894         var self = this;
4895         this.selected_ids = ids || [];
4896         if(this.selected_ids.length > 0) {
4897             self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4898         } else {
4899             self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4900         }
4901     },
4902     new_object: function() {
4903         if (this.searchview) {
4904             this.searchview.hide();
4905         }
4906         if (this.view_list) {
4907             this.view_list.do_hide();
4908         }
4909         this.setup_form_view();
4910     },
4911 });
4912
4913 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4914     do_add_record: function () {
4915         this.popup.new_object();
4916     },
4917     select_record: function(index) {
4918         this.popup.select_elements([this.dataset.ids[index]]);
4919         this.popup.destroy();
4920     },
4921     do_select: function(ids, records) {
4922         this._super(ids, records);
4923         this.popup.on_click_element(ids);
4924     }
4925 });
4926
4927 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4928     template: 'FieldReference',
4929     init: function(field_manager, node) {
4930         this._super(field_manager, node);
4931         this.reference_ready = true;
4932     },
4933     destroy_content: function() {
4934         if (this.fm) {
4935             this.fm.destroy();
4936             this.fm = undefined;
4937         }
4938     },
4939     initialize_content: function() {
4940         var self = this;
4941         var fm = new instance.web.form.DefaultFieldManager(this);
4942         this.fm = fm;
4943         fm.extend_field_desc({
4944             "selection": {
4945                 selection: this.field_manager.get_field_desc(this.name).selection,
4946                 type: "selection",
4947             },
4948             "m2o": {
4949                 relation: null,
4950                 type: "many2one",
4951             },
4952         });
4953         this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
4954             name: 'selection',
4955             modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4956         }});
4957         this.selection.on("change:value", this, this.on_selection_changed);
4958         this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
4959         this.selection
4960             .on('focused', null, function () {self.trigger('focused')})
4961             .on('blurred', null, function () {self.trigger('blurred')});
4962
4963         this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
4964             name: 'Referenced Document',
4965             modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
4966         }});
4967         this.m2o.on("change:value", this, this.data_changed);
4968         this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
4969         this.m2o
4970             .on('focused', null, function () {self.trigger('focused')})
4971             .on('blurred', null, function () {self.trigger('blurred')});
4972     },
4973     on_selection_changed: function() {
4974         if (this.reference_ready) {
4975             this.internal_set_value([this.selection.get_value(), false]);
4976             this.render_value();
4977         }
4978     },
4979     data_changed: function() {
4980         if (this.reference_ready) {
4981             this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
4982         }
4983     },
4984     set_value: function(val) {
4985         if (val) {
4986             val = val.split(',');
4987             val[0] = val[0] || false;
4988             val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
4989         }
4990         this._super(val || [false, false]);
4991     },
4992     get_value: function() {
4993         return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
4994     },
4995     render_value: function() {
4996         this.reference_ready = false;
4997         if (!this.get("effective_readonly")) {
4998             this.selection.set_value(this.get('value')[0]);
4999         }
5000         this.m2o.field.relation = this.get('value')[0];
5001         this.m2o.set_value(this.get('value')[1]);
5002         this.m2o.$el.toggle(!!this.get('value')[0]);
5003         this.reference_ready = true;
5004     },
5005 });
5006
5007 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5008     init: function(field_manager, node) {
5009         var self = this;
5010         this._super(field_manager, node);
5011         this.binary_value = false;
5012         this.useFileAPI = !!window.FileReader;
5013         this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5014         if (!this.useFileAPI) {
5015             this.fileupload_id = _.uniqueId('oe_fileupload');
5016             $(window).on(this.fileupload_id, function() {
5017                 var args = [].slice.call(arguments).slice(1);
5018                 self.on_file_uploaded.apply(self, args);
5019             });
5020         }
5021     },
5022     stop: function() {
5023         if (!this.useFileAPI) {
5024             $(window).off(this.fileupload_id);
5025         }
5026         this._super.apply(this, arguments);
5027     },
5028     initialize_content: function() {
5029         this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5030         this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5031         this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5032     },
5033     on_file_change: function(e) {
5034         var self = this;
5035         var file_node = e.target;
5036         if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5037             if (this.useFileAPI) {
5038                 var file = file_node.files[0];
5039                 if (file.size > this.max_upload_size) {
5040                     var msg = _t("The selected file exceed the maximum file size of %s.");
5041                     instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5042                     return false;
5043                 }
5044                 var filereader = new FileReader();
5045                 filereader.readAsDataURL(file);
5046                 filereader.onloadend = function(upload) {
5047                     var data = upload.target.result;
5048                     data = data.split(',')[1];
5049                     self.on_file_uploaded(file.size, file.name, file.type, data);
5050                 };
5051             } else {
5052                 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5053                 this.$el.find('form.oe_form_binary_form').submit();
5054             }
5055             this.$el.find('.oe_form_binary_progress').show();
5056             this.$el.find('.oe_form_binary').hide();
5057         }
5058     },
5059     on_file_uploaded: function(size, name, content_type, file_base64) {
5060         if (size === false) {
5061             this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5062             // TODO: use openerp web crashmanager
5063             console.warn("Error while uploading file : ", name);
5064         } else {
5065             this.filename = name;
5066             this.on_file_uploaded_and_valid.apply(this, arguments);
5067         }
5068         this.$el.find('.oe_form_binary_progress').hide();
5069         this.$el.find('.oe_form_binary').show();
5070     },
5071     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5072     },
5073     on_save_as: function(ev) {
5074         var value = this.get('value');
5075         if (!value) {
5076             this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5077             ev.stopPropagation();
5078         } else {
5079             instance.web.blockUI();
5080             var c = instance.webclient.crashmanager;
5081             this.session.get_file({
5082                 url: '/web/binary/saveas_ajax',
5083                 data: {data: JSON.stringify({
5084                     model: this.view.dataset.model,
5085                     id: (this.view.datarecord.id || ''),
5086                     field: this.name,
5087                     filename_field: (this.node.attrs.filename || ''),
5088                     data: instance.web.form.is_bin_size(value) ? null : value,
5089                     context: this.view.dataset.get_context()
5090                 })},
5091                 complete: instance.web.unblockUI,
5092                 error: c.rpc_error.bind(c)
5093             });
5094             ev.stopPropagation();
5095             return false;
5096         }
5097     },
5098     set_filename: function(value) {
5099         var filename = this.node.attrs.filename;
5100         if (filename) {
5101             var tmp = {};
5102             tmp[filename] = value;
5103             this.field_manager.set_values(tmp);
5104         }
5105     },
5106     on_clear: function() {
5107         if (this.get('value') !== false) {
5108             this.binary_value = false;
5109             this.internal_set_value(false);
5110         }
5111         return false;
5112     }
5113 });
5114
5115 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5116     template: 'FieldBinaryFile',
5117     initialize_content: function() {
5118         this._super();
5119         if (this.get("effective_readonly")) {
5120             var self = this;
5121             this.$el.find('a').click(function(ev) {
5122                 if (self.get('value')) {
5123                     self.on_save_as(ev);
5124                 }
5125                 return false;
5126             });
5127         }
5128     },
5129     render_value: function() {
5130         if (!this.get("effective_readonly")) {
5131             var show_value;
5132             if (this.node.attrs.filename) {
5133                 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5134             } else {
5135                 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5136             }
5137             this.$el.find('input').eq(0).val(show_value);
5138         } else {
5139             this.$el.find('a').toggle(!!this.get('value'));
5140             if (this.get('value')) {
5141                 var show_value = _t("Download")
5142                 if (this.view)
5143                     show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5144                 this.$el.find('a').text(show_value);
5145             }
5146         }
5147     },
5148     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5149         this.binary_value = true;
5150         this.internal_set_value(file_base64);
5151         var show_value = name + " (" + instance.web.human_size(size) + ")";
5152         this.$el.find('input').eq(0).val(show_value);
5153         this.set_filename(name);
5154     },
5155     on_clear: function() {
5156         this._super.apply(this, arguments);
5157         this.$el.find('input').eq(0).val('');
5158         this.set_filename('');
5159     }
5160 });
5161
5162 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5163     template: 'FieldBinaryImage',
5164     placeholder: "/web/static/src/img/placeholder.png",
5165     render_value: function() {
5166         var self = this;
5167         var url;
5168         if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5169             url = 'data:image/png;base64,' + this.get('value');
5170         } else if (this.get('value')) {
5171             var id = JSON.stringify(this.view.datarecord.id || null);
5172             var field = this.name;
5173             if (this.options.preview_image)
5174                 field = this.options.preview_image;
5175             url = this.session.url('/web/binary/image', {
5176                                         model: this.view.dataset.model,
5177                                         id: id,
5178                                         field: field,
5179                                         t: (new Date().getTime()),
5180             });
5181         } else {
5182             url = this.placeholder;
5183         }
5184         var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5185         this.$el.find('> img').remove();
5186         this.$el.prepend($img);
5187         $img.load(function() {
5188             if (! self.options.size)
5189                 return;
5190             $img.css("max-width", "" + self.options.size[0] + "px");
5191             $img.css("max-height", "" + self.options.size[1] + "px");
5192             $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5193             $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5194         });
5195         $img.on('error', function() {
5196             $img.attr('src', self.placeholder);
5197             instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5198         });
5199     },
5200     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5201         this.internal_set_value(file_base64);
5202         this.binary_value = true;
5203         this.render_value();
5204         this.set_filename(name);
5205     },
5206     on_clear: function() {
5207         this._super.apply(this, arguments);
5208         this.render_value();
5209         this.set_filename('');
5210     },
5211     set_value: function(value_){
5212         var changed = value_ !== this.get_value();
5213         this._super.apply(this, arguments);
5214         // By default, on binary images read, the server returns the binary size
5215         // This is possible that two images have the exact same size
5216         // Therefore we trigger the change in case the image value hasn't changed
5217         // So the image is re-rendered correctly
5218         if (!changed){
5219             this.trigger("change:value", this, {
5220                 oldValue: value_,
5221                 newValue: value_
5222             });
5223         }
5224     }
5225 });
5226
5227 /**
5228  * Widget for (one2many field) to upload one or more file in same time and display in list.
5229  * The user can delete his files.
5230  * Options on attribute ; "blockui" {Boolean} block the UI or not
5231  * during the file is uploading
5232  */
5233 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5234     template: "FieldBinaryFileUploader",
5235     init: function(field_manager, node) {
5236         this._super(field_manager, node);
5237         this.field_manager = field_manager;
5238         this.node = node;
5239         if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5240             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);
5241         }
5242         this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5243         this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5244         $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5245     },
5246     initialize_content: function() {
5247         this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5248     },
5249     set_value: function(value_) {
5250         var value_ = value_ || [];
5251         var self = this;
5252         var ids = [];
5253         _.each(value_, function(command) {
5254             if (isNaN(command) && command.id == undefined) {
5255                 switch (command[0]) {
5256                     case commands.CREATE:
5257                         ids = ids.concat(command[2]);
5258                         return;
5259                     case commands.REPLACE_WITH:
5260                         ids = ids.concat(command[2]);
5261                         return;
5262                     case commands.UPDATE:
5263                         ids = ids.concat(command[2]);
5264                         return;
5265                     case commands.LINK_TO:
5266                         ids = ids.concat(command[1]);
5267                         return;
5268                     case commands.DELETE:
5269                         ids = _.filter(ids, function (id) { return id != command[1];});
5270                         return;
5271                     case commands.DELETE_ALL:
5272                         ids = [];
5273                         return;
5274                 }
5275             } else {
5276                 ids.push(command);
5277             }
5278         });
5279         this._super( ids );
5280     },
5281     get_value: function() {
5282         return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
5283     },
5284     get_file_url: function (attachment) {
5285         return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5286     },
5287     read_name_values : function () {
5288         var self = this;
5289         // select the list of id for a get_name
5290         var values = [];
5291         _.each(this.get('value'), function (val) {
5292             if (typeof val != 'object') {
5293                 values.push(val);
5294             }
5295         });
5296         // send request for get_name
5297         if (values.length) {
5298             return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
5299                 _.each(datas, function (data) {
5300                     data.no_unlink = true;
5301                     data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5302
5303                     _.each(self.get('value'), function (val, key) {
5304                         if(val == data.id) {
5305                             self.get('value')[key] = data;
5306                         }
5307                     });
5308                 });
5309             });
5310         } else {
5311             return $.when(this.get('value'));
5312         }
5313     },
5314     render_value: function () {
5315         var self = this;
5316         this.read_name_values().then(function (datas) {
5317
5318             var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5319             render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5320             self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5321
5322             // reinit input type file
5323             var $input = self.$('input.oe_form_binary_file');
5324             $input.after($input.clone(true)).remove();
5325             self.$(".oe_fileupload").show();
5326
5327         });
5328     },
5329     on_file_change: function (event) {
5330         event.stopPropagation();
5331         var self = this;
5332         var $target = $(event.target);
5333         if ($target.val() !== '') {
5334
5335             var filename = $target.val().replace(/.*[\\\/]/,'');
5336
5337             // if the files is currently uploded, don't send again
5338             if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
5339                 return false;
5340             }
5341
5342             // block UI or not
5343             if(this.node.attrs.blockui>0) {
5344                 instance.web.blockUI();
5345             }
5346
5347             // if the files exits for this answer, delete the file before upload
5348             var files = _.filter(this.get('value'), function (file) {
5349                 if((file.filename || file.name) == filename) {
5350                     self.ds_file.unlink([file.id]);
5351                     return false;
5352                 } else {
5353                     return true;
5354                 }
5355             });
5356
5357             // TODO : unactivate send on wizard and form
5358
5359             // submit file
5360             this.$('form.oe_form_binary_form').submit();
5361             this.$(".oe_fileupload").hide();
5362
5363             // add file on result
5364             files.push({
5365                 'id': 0,
5366                 'name': filename,
5367                 'filename': filename,
5368                 'url': '',
5369                 'upload': true
5370             });
5371
5372             this.set({'value': files});
5373         }
5374     },
5375     on_file_loaded: function (event, result) {
5376         var files = this.get('value');
5377
5378         // unblock UI
5379         if(this.node.attrs.blockui>0) {
5380             instance.web.unblockUI();
5381         }
5382
5383         // TODO : activate send on wizard and form
5384
5385         if (result.error || !result.id ) {
5386             this.do_warn( _t('Uploading Error'), result.error);
5387             files = _.filter(files, function (val) { return !val.upload; });
5388         } else {
5389             for(var i in files){
5390                 if(files[i].filename == result.filename && files[i].upload) {
5391                     files[i] = {
5392                         'id': result.id,
5393                         'name': result.name,
5394                         'filename': result.filename,
5395                         'url': this.get_file_url(result)
5396                     };
5397                 }
5398             }
5399         }
5400
5401         this.set({'value': files});
5402         this.render_value()
5403     },
5404     on_file_delete: function (event) {
5405         event.stopPropagation();
5406         var file_id=$(event.target).data("id");
5407         if (file_id) {
5408             var files=[];
5409             for(var i in this.get('value')){
5410                 if(file_id != this.get('value')[i].id){
5411                     files.push(this.get('value')[i]);
5412                 }
5413                 else if(!this.get('value')[i].no_unlink) {
5414                     this.ds_file.unlink([file_id]);
5415                 }
5416             }
5417             this.set({'value': files});
5418         }
5419     },
5420 });
5421
5422 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5423     template: "FieldStatus",
5424     init: function(field_manager, node) {
5425         this._super(field_manager, node);
5426         this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5427         this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5428         this.set({value: false});
5429         this.selection = [];
5430         this.set("selection", []);
5431         this.selection_dm = new instance.web.DropMisordered();
5432     },
5433     start: function() {
5434         this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5435         this.calc_domain();
5436         this.on("change:value", this, this.get_selection);
5437         this.on("change:evaluated_selection_domain", this, this.get_selection);
5438         this.on("change:selection", this, function() {
5439             this.selection = this.get("selection");
5440             this.render_value();
5441         });
5442         this.get_selection();
5443         if (this.options.clickable) {
5444             this.$el.on('click','li',this.on_click_stage);
5445         }
5446         this._super();
5447     },
5448     set_value: function(value_) {
5449         if (value_ instanceof Array) {
5450             value_ = value_[0];
5451         }
5452         this._super(value_);
5453     },
5454     render_value: function() {
5455         var self = this;
5456         var content = QWeb.render("FieldStatus.content", {widget: self});
5457         self.$el.html(content);
5458         var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
5459         var color = colors[self.get('value')];
5460         if (color) {
5461             self.$("oe_active").css("color", color);
5462         }
5463     },
5464     calc_domain: function() {
5465         var d = instance.web.pyeval.eval('domain', this.build_domain());
5466         var domain = []; //if there is no domain defined, fetch all the records
5467         
5468         if (d.length) {
5469             domain = ['|',['id', '=', this.get('value')]].concat(d);
5470         }
5471         
5472         if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5473             this.set("evaluated_selection_domain", domain);
5474         }
5475     },
5476     /** Get the selection and render it
5477      *  selection: [[identifier, value_to_display], ...]
5478      *  For selection fields: this is directly given by this.field.selection
5479      *  For many2one fields:  perform a search on the relation of the many2one field
5480      */
5481     get_selection: function() {
5482         var self = this;
5483         var selection = [];
5484
5485         var calculation = _.bind(function() {
5486             if (this.field.type == "many2one") {
5487                 var domain = [];
5488                 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5489                     self.build_context(), this.get("evaluated_selection_domain"));
5490                 return ds.read_slice(['name'], {}).then(function (records) {
5491                     for(var i = 0; i < records.length; i++) {
5492                         selection.push([records[i].id, records[i].name]);
5493                     }
5494                 });
5495             } else {
5496                 // For field type selection filter values according to
5497                 // statusbar_visible attribute of the field. For example:
5498                 // statusbar_visible="draft,open".
5499                 var select = this.field.selection;
5500                 for(var i=0; i < select.length; i++) {
5501                     var key = select[i][0];
5502                     if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5503                         selection.push(select[i]);
5504                     }
5505                 }
5506                 return $.when();
5507             }
5508         }, this);
5509         this.selection_dm.add(calculation()).then(function () {
5510             if (! _.isEqual(selection, self.get("selection"))) {
5511                 self.set("selection", selection);
5512             }
5513         });
5514     },
5515     on_click_stage: function (ev) {
5516         var self = this;
5517         var $li = $(ev.currentTarget);
5518         if (this.field.type == "many2one") {
5519             var val = parseInt($li.data("id"));
5520         }
5521         else {
5522             var val = $li.data("id");
5523         }
5524         if (val != self.get('value')) {
5525             this.view.recursive_save().done(function() {
5526                 var change = {};
5527                 change[self.name] = val;
5528                 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5529                     self.view.reload();
5530                 });
5531             });
5532         }
5533     },
5534 });
5535
5536 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5537     template: "FieldMonetary",
5538     widget_class: 'oe_form_field_float oe_form_field_monetary',
5539     init: function() {
5540         this._super.apply(this, arguments);
5541         this.set({"currency": false});
5542         if (this.options.currency_field) {
5543             this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5544                 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5545             });
5546         }
5547         this.on("change:currency", this, this.get_currency_info);
5548         this.get_currency_info();
5549         this.ci_dm = new instance.web.DropMisordered();
5550     },
5551     start: function() {
5552         var tmp = this._super();
5553         this.on("change:currency_info", this, this.reinitialize);
5554         return tmp;
5555     },
5556     get_currency_info: function() {
5557         var self = this;
5558         if (this.get("currency") === false) {
5559             this.set({"currency_info": null});
5560             return;
5561         }
5562         return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5563             .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5564             self.set({"currency_info": res});
5565         });
5566     },
5567     parse_value: function(val, def) {
5568         return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5569     },
5570     format_value: function(val, def) {
5571         return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5572     },
5573 });
5574
5575 /**
5576  * Registry of form fields, called by :js:`instance.web.FormView`.
5577  *
5578  * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5579  * will substitute to the <field> tags as defined in OpenERP's views.
5580  */
5581 instance.web.form.widgets = new instance.web.Registry({
5582     'char' : 'instance.web.form.FieldChar',
5583     'id' : 'instance.web.form.FieldID',
5584     'email' : 'instance.web.form.FieldEmail',
5585     'url' : 'instance.web.form.FieldUrl',
5586     'text' : 'instance.web.form.FieldText',
5587     'html' : 'instance.web.form.FieldTextHtml',
5588     'date' : 'instance.web.form.FieldDate',
5589     'datetime' : 'instance.web.form.FieldDatetime',
5590     'selection' : 'instance.web.form.FieldSelection',
5591     'many2one' : 'instance.web.form.FieldMany2One',
5592     'many2onebutton' : 'instance.web.form.Many2OneButton',
5593     'many2many' : 'instance.web.form.FieldMany2Many',
5594     'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5595     'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5596     'one2many' : 'instance.web.form.FieldOne2Many',
5597     'one2many_list' : 'instance.web.form.FieldOne2Many',
5598     'reference' : 'instance.web.form.FieldReference',
5599     'boolean' : 'instance.web.form.FieldBoolean',
5600     'float' : 'instance.web.form.FieldFloat',
5601     'integer': 'instance.web.form.FieldFloat',
5602     'float_time': 'instance.web.form.FieldFloat',
5603     'progressbar': 'instance.web.form.FieldProgressBar',
5604     'image': 'instance.web.form.FieldBinaryImage',
5605     'binary': 'instance.web.form.FieldBinaryFile',
5606     'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5607     'statusbar': 'instance.web.form.FieldStatus',
5608     'monetary': 'instance.web.form.FieldMonetary',
5609 });
5610
5611 /**
5612  * Registry of widgets usable in the form view that can substitute to any possible
5613  * tags defined in OpenERP's form views.
5614  *
5615  * Every referenced class should extend FormWidget.
5616  */
5617 instance.web.form.tags = new instance.web.Registry({
5618     'button' : 'instance.web.form.WidgetButton',
5619 });
5620
5621 instance.web.form.custom_widgets = new instance.web.Registry({
5622 });
5623
5624 };
5625
5626 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: