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