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