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