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