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