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