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