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