[MERGE] forward port of branch 8.0 up to c92e70b
[odoo/odoo.git] / addons / web / static / src / js / view_form.js
1 (function() {
2
3 var instance = openerp;
4 var _t = instance.web._t,
5    _lt = instance.web._lt;
6 var QWeb = instance.web.qweb;
7
8 /** @namespace */
9 instance.web.form = {};
10
11 /**
12  * Interface implemented by the form view or any other object
13  * able to provide the features necessary for the fields to work.
14  *
15  * Properties:
16  *     - display_invalid_fields : if true, all fields where is_valid() return true should
17  *     be displayed as invalid.
18  *     - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
19  * Events:
20  *     - view_content_has_changed : when the values of the fields have changed. When
21  *     this event is triggered all fields should reprocess their modifiers.
22  *     - field_changed:<field_name> : when the value of a field change, an event is triggered
23  *     named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
24  *     This event is not related to the on_change mechanism of OpenERP and is always called
25  *     when the value of a field is setted or changed. This event is only triggered when the
26  *     value of the field is syntactically valid, but it can be triggered when the value
27  *     is sematically invalid (ie, when a required field is false). It is possible that an event
28  *     about a precise field is never triggered even if that field exists in the view, in that
29  *     case the value of the field is assumed to be false.
30  */
31 instance.web.form.FieldManagerMixin = {
32     /**
33      * Must return the asked field as in fields_get.
34      */
35     get_field_desc: function(field_name) {},
36     /**
37      * Returns the current value of a field present in the view. See the get_value() method
38      * method in FieldInterface for further information.
39      */
40     get_field_value: function(field_name) {},
41     /**
42     Gives new values for the fields contained in the view. The new values could not be setted
43     right after the call to this method. Setting new values can trigger on_changes.
44
45     @param {Object} values A dictonary with key = field name and value = new value.
46     @return {$.Deferred} Is resolved after all the values are setted.
47     */
48     set_values: function(values) {},
49     /**
50     Computes an OpenERP domain.
51
52     @param {Array} expression An OpenERP domain.
53     @return {boolean} The computed value of the domain.
54     */
55     compute_domain: function(expression) {},
56     /**
57     Builds an evaluation context for the resolution of the fields' contexts. Please note
58     the field are only supposed to use this context to evualuate their own, they should not
59     extend it.
60
61     @return {CompoundContext} An OpenERP context.
62     */
63     build_eval_context: function() {},
64 };
65
66 instance.web.views.add('form', 'instance.web.FormView');
67 /**
68  * Properties:
69  *      - actual_mode: always "view", "edit" or "create". Read-only property. Determines
70  *      the mode used by the view.
71  */
72 instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
73     /**
74      * Indicates that this view is not searchable, and thus that no search
75      * view should be displayed (if there is one active).
76      */
77     searchable: false,
78     template: "FormView",
79     display_name: _lt('Form'),
80     view_type: "form",
81     /**
82      * @constructs instance.web.FormView
83      * @extends instance.web.View
84      *
85      * @param {instance.web.Session} session the current openerp session
86      * @param {instance.web.DataSet} dataset the dataset this view will work with
87      * @param {String} view_id the identifier of the OpenERP view object
88      *
89      * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
90      */
91     init: function(parent, dataset, view_id, options) {
92         var self = this;
93         this._super(parent);
94         this.ViewManager = parent;
95         this.set_default_options(options);
96         this.dataset = dataset;
97         this.model = dataset.model;
98         this.view_id = view_id || false;
99         this.fields_view = {};
100         this.fields = {};
101         this.fields_order = [];
102         this.datarecord = {};
103         this._onchange_specs = {};
104         this.onchanges_defs = [];
105         this.default_focus_field = null;
106         this.default_focus_button = null;
107         this.fields_registry = instance.web.form.widgets;
108         this.tags_registry = instance.web.form.tags;
109         this.widgets_registry = instance.web.form.custom_widgets;
110         this.has_been_loaded = $.Deferred();
111         this.translatable_fields = [];
112         _.defaults(this.options, {
113             "not_interactible_on_create": false,
114             "initial_mode": "view",
115             "disable_autofocus": false,
116             "footer_to_buttons": false,
117         });
118         this.is_initialized = $.Deferred();
119         this.mutating_mutex = new $.Mutex();
120         this.save_list = [];
121         this.render_value_defs = [];
122         this.reload_mutex = new $.Mutex();
123         this.__clicked_inside = false;
124         this.__blur_timeout = null;
125         this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
126         self.set({actual_mode: self.options.initial_mode});
127         this.has_been_loaded.done(function() {
128             self._build_onchange_specs();
129             self.on("change:actual_mode", self, self.check_actual_mode);
130             self.check_actual_mode();
131             self.on("change:actual_mode", self, self.init_pager);
132             self.init_pager();
133         });
134         self.on("load_record", self, self.load_record);
135         instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
136             if (!this.can_be_discarded()) {
137                 e.preventDefault();
138             }
139         });
140     },
141     view_loading: function(r) {
142         return this.load_form(r);
143     },
144     destroy: function() {
145         _.each(this.get_widgets(), function(w) {
146             w.off('focused blurred');
147             w.destroy();
148         });
149         if (this.$el) {
150             this.$el.off('.formBlur');
151         }
152         this._super();
153     },
154     load_form: function(data) {
155         var self = this;
156         if (!data) {
157             throw new Error(_t("No data provided."));
158         }
159         if (this.arch) {
160             throw "Form view does not support multiple calls to load_form";
161         }
162         this.fields_order = [];
163         this.fields_view = data;
164
165         this.rendering_engine.set_fields_registry(this.fields_registry);
166         this.rendering_engine.set_tags_registry(this.tags_registry);
167         this.rendering_engine.set_widgets_registry(this.widgets_registry);
168         this.rendering_engine.set_fields_view(data);
169         var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
170         this.rendering_engine.render_to($dest);
171
172         this.$el.on('mousedown.formBlur', function () {
173             self.__clicked_inside = true;
174         });
175
176         this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
177         if (this.options.$buttons) {
178             this.$buttons.appendTo(this.options.$buttons);
179         } else {
180             this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
181         }
182         this.$buttons.on('click', '.oe_form_button_create',
183                          this.guard_active(this.on_button_create));
184         this.$buttons.on('click', '.oe_form_button_edit',
185                          this.guard_active(this.on_button_edit));
186         this.$buttons.on('click', '.oe_form_button_save',
187                          this.guard_active(this.on_button_save));
188         this.$buttons.on('click', '.oe_form_button_cancel',
189                          this.guard_active(this.on_button_cancel));
190         if (this.options.footer_to_buttons) {
191             this.$el.find('footer').appendTo(this.$buttons);
192         }
193
194         this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
195         if (!this.sidebar && this.options.$sidebar) {
196             this.sidebar = new instance.web.Sidebar(this);
197             this.sidebar.appendTo(this.$sidebar);
198             if (this.fields_view.toolbar) {
199                 this.sidebar.add_toolbar(this.fields_view.toolbar);
200             }
201             this.sidebar.add_items('other', _.compact([
202                 self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
203                 self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
204             ]));
205         }
206
207         this.has_been_loaded.resolve();
208
209         // Add bounce effect on button 'Edit' when click on readonly page view.
210         this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) {
211             if(self.get("actual_mode") == "view") {
212                 var $button = self.options.$buttons.find(".oe_form_button_edit");
213                 $button.openerpBounce();
214                 e.stopPropagation();
215                 instance.web.bus.trigger('click', e);
216             }
217         });
218         //bounce effect on red button when click on statusbar.
219         this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
220             if((self.get("actual_mode") == "view")) {
221                 var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
222                 $button.openerpBounce();
223                 e.stopPropagation();
224             }
225          });
226         this.trigger('form_view_loaded', data);
227         return $.when();
228     },
229     widgetFocused: function() {
230         // Clear click flag if used to focus a widget
231         this.__clicked_inside = false;
232         if (this.__blur_timeout) {
233             clearTimeout(this.__blur_timeout);
234             this.__blur_timeout = null;
235         }
236     },
237     widgetBlurred: function() {
238         if (this.__clicked_inside) {
239             // clicked in an other section of the form (than the currently
240             // focused widget) => just ignore the blurring entirely?
241             this.__clicked_inside = false;
242             return;
243         }
244         var self = this;
245         // clear timeout, if any
246         this.widgetFocused();
247         this.__blur_timeout = setTimeout(function () {
248             self.trigger('blurred');
249         }, 0);
250     },
251
252     do_load_state: function(state, warm) {
253         if (state.id && this.datarecord.id != state.id) {
254             if (this.dataset.get_id_index(state.id) === null) {
255                 this.dataset.ids.push(state.id);
256             }
257             this.dataset.select_id(state.id);
258             this.do_show();
259         }
260     },
261     /**
262      *
263      * @param {Object} [options]
264      * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
265      * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
266      * @return {$.Deferred}
267      */
268     do_show: function (options) {
269         var self = this;
270         options = options || {};
271         if (this.sidebar) {
272             this.sidebar.$el.show();
273         }
274         if (this.$buttons) {
275             this.$buttons.show();
276         }
277         this.$el.show().css({
278             opacity: '0',
279             filter: 'alpha(opacity = 0)'
280         });
281         this.$el.add(this.$buttons).removeClass('oe_form_dirty');
282
283         var shown = this.has_been_loaded;
284         if (options.reload !== false) {
285             shown = shown.then(function() {
286                 if (self.dataset.index === null) {
287                     // null index means we should start a new record
288                     return self.on_button_new();
289                 }
290                 var fields = _.keys(self.fields_view.fields);
291                 fields.push('display_name');
292                 return self.dataset.read_index(fields, {
293                     context: { 'bin_size': true, 'future_display_name' : true }
294                 }).then(function(r) {
295                     self.trigger('load_record', r);
296                 });
297             });
298         }
299         return shown.then(function() {
300             self._actualize_mode(options.mode || self.options.initial_mode);
301             self.$el.css({
302                 opacity: '1',
303                 filter: 'alpha(opacity = 100)'
304             });
305             instance.web.bus.trigger('form_view_shown', self);
306         });
307     },
308     do_hide: function () {
309         if (this.sidebar) {
310             this.sidebar.$el.hide();
311         }
312         if (this.$buttons) {
313             this.$buttons.hide();
314         }
315         if (this.$pager) {
316             this.$pager.hide();
317         }
318         this._super();
319     },
320     load_record: function(record) {
321         var self = this, set_values = [];
322         if (!record) {
323             this.set({ 'title' : undefined });
324             this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
325             return $.Deferred().reject();
326         }
327         this.datarecord = record;
328         this._actualize_mode();
329         this.set({ 'title' : record.id ? record.display_name : _t("New") });
330
331         _(this.fields).each(function (field, f) {
332             field._dirty_flag = false;
333             field._inhibit_on_change_flag = true;
334             var result = field.set_value(self.datarecord[f] || false);
335             field._inhibit_on_change_flag = false;
336             set_values.push(result);
337         });
338         return $.when.apply(null, set_values).then(function() {
339             if (!record.id) {
340                 // trigger onchanges
341                 self.do_onchange(null);
342             }
343             self.on_form_changed();
344             self.rendering_engine.init_fields();
345             self.is_initialized.resolve();
346             self.do_update_pager(record.id === null || record.id === undefined);
347             if (self.sidebar) {
348                self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
349             }
350             if (record.id) {
351                 self.do_push_state({id:record.id});
352             } else {
353                 self.do_push_state({});
354             }
355             self.$el.add(self.$buttons).removeClass('oe_form_dirty');
356             self.autofocus();
357         });
358     },
359     /**
360      * Loads and sets up the default values for the model as the current
361      * record
362      *
363      * @return {$.Deferred}
364      */
365     load_defaults: function () {
366         var self = this;
367         var keys = _.keys(this.fields_view.fields);
368         if (keys.length) {
369             return this.dataset.default_get(keys).then(function(r) {
370                 self.trigger('load_record', r);
371             });
372         }
373         return self.trigger('load_record', {});
374     },
375     on_form_changed: function() {
376         this.trigger("view_content_has_changed");
377     },
378     do_notify_change: function() {
379         this.$el.add(this.$buttons).addClass('oe_form_dirty');
380     },
381     execute_pager_action: function(action) {
382         if (this.can_be_discarded()) {
383             switch (action) {
384                 case 'first':
385                     this.dataset.index = 0;
386                     break;
387                 case 'previous':
388                     this.dataset.previous();
389                     break;
390                 case 'next':
391                     this.dataset.next();
392                     break;
393                 case 'last':
394                     this.dataset.index = this.dataset.ids.length - 1;
395                     break;
396             }
397             var def = this.reload();
398             this.trigger('pager_action_executed');
399             return def;
400         }
401         return $.when();
402     },
403     init_pager: function() {
404         var self = this;
405         if (this.$pager)
406             this.$pager.remove();
407         if (this.get("actual_mode") === "create")
408             return;
409         this.$pager = $(QWeb.render("FormView.pager", {'widget':self}));
410         if (this.options.$pager) {
411             this.$pager.appendTo(this.options.$pager);
412         } else {
413             this.$el.find('.oe_form_pager').replaceWith(this.$pager);
414         }
415         this.$pager.on('click','a[data-pager-action]',function() {
416             var $el = $(this);
417             if ($el.attr("disabled"))
418                 return;
419             var action = $el.data('pager-action');
420             var def = $.when(self.execute_pager_action(action));
421             $el.attr("disabled");
422             def.always(function() {
423                 $el.removeAttr("disabled");
424             });
425         });
426         this.do_update_pager();
427     },
428     do_update_pager: function(hide_index) {
429         this.$pager.toggle(this.dataset.ids.length > 1);
430         if (hide_index) {
431             $(".oe_form_pager_state", this.$pager).html("");
432         } else {
433             $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
434         }
435     },
436
437     _build_onchange_specs: function() {
438         var self = this;
439         var find = function(field_name, root) {
440             var fields = [root];
441             while (fields.length) {
442                 var node = fields.pop();
443                 if (!node) {
444                     continue;
445                 }
446                 if (node.tag === 'field' && node.attrs.name === field_name) {
447                     return node.attrs.on_change || "";
448                 }
449                 fields = _.union(fields, node.children);
450             }
451             return "";
452         };
453
454         self._onchange_specs = {};
455         _.each(this.fields, function(field, name) {
456             self._onchange_specs[name] = find(name, field.node);
457             _.each(field.field.views, function(view) {
458                 _.each(view.fields, function(_, subname) {
459                     self._onchange_specs[name + '.' + subname] = find(subname, view.arch);
460                 });
461             });
462         });
463     },
464     _get_onchange_values: function() {
465         var field_values = this.get_fields_values();
466         if (field_values.id.toString().match(instance.web.BufferedDataSet.virtual_id_regex)) {
467             delete field_values.id;
468         }
469         if (this.dataset.parent_view) {
470             // this belongs to a parent view: add parent field if possible
471             var parent_view = this.dataset.parent_view;
472             var child_name = this.dataset.child_name;
473             var parent_name = parent_view.get_field_desc(child_name).relation_field;
474             if (parent_name) {
475                 // consider all fields except the inverse of the parent field
476                 var parent_values = parent_view.get_fields_values();
477                 delete parent_values[child_name];
478                 field_values[parent_name] = parent_values;
479             }
480         }
481         return field_values;
482     },
483
484     do_onchange: function(widget) {
485         var self = this;
486         var onchange_specs = self._onchange_specs;
487         try {
488             var def = $.when({});
489             var change_spec = widget ? onchange_specs[widget.name] : null;
490             if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) {
491                 var ids = [],
492                     trigger_field_name = widget ? widget.name : false,
493                     values = self._get_onchange_values(),
494                     context = new instance.web.CompoundContext(self.dataset.get_context());
495
496                 if (widget && widget.build_context()) {
497                     context.add(widget.build_context());
498                 }
499                 if (self.dataset.parent_view) {
500                     var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field;
501                     context.add({field_parent: parent_name});
502                 }
503
504                 if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
505                     // In case of a o2m virtual id, we should pass an empty ids list
506                     ids.push(self.datarecord.id);
507                 }
508                 def = self.alive(new instance.web.Model(self.dataset.model).call(
509                     "onchange", [ids, values, trigger_field_name, onchange_specs, context]));
510             }
511             var onchange_def = def.then(function(response) {
512                 if (widget && widget.field['change_default']) {
513                     var fieldname = widget.name;
514                     var value_;
515                     if (response.value && (fieldname in response.value)) {
516                         // Use value from onchange if onchange executed
517                         value_ = response.value[fieldname];
518                     } else {
519                         // otherwise get form value for field
520                         value_ = self.fields[fieldname].get_value();
521                     }
522                     var condition = fieldname + '=' + value_;
523
524                     if (value_) {
525                         return self.alive(new instance.web.Model('ir.values').call(
526                             'get_defaults', [self.model, condition]
527                         )).then(function (results) {
528                             if (!results.length) {
529                                 return response;
530                             }
531                             if (!response.value) {
532                                 response.value = {};
533                             }
534                             for(var i=0; i<results.length; ++i) {
535                                 // [whatever, key, value]
536                                 var triplet = results[i];
537                                 response.value[triplet[1]] = triplet[2];
538                             }
539                             return response;
540                         });
541                     }
542                 }
543                 return response;
544             }).then(function(response) {
545                 return self.on_processed_onchange(response);
546             });
547             this.onchanges_defs.push(onchange_def);
548             return onchange_def;
549         } catch(e) {
550             console.error(e);
551             instance.webclient.crashmanager.show_message(e);
552             return $.Deferred().reject();
553         }
554     },
555     on_processed_onchange: function(result) {
556         try {
557         var fields = this.fields;
558         _(result.domain).each(function (domain, fieldname) {
559             var field = fields[fieldname];
560             if (!field) { return; }
561             field.node.attrs.domain = domain;
562         });
563
564         if (!_.isEmpty(result.value)) {
565             this._internal_set_values(result.value);
566         }
567         // FIXME XXX a list of warnings?
568         if (!_.isEmpty(result.warning)) {
569             new instance.web.Dialog(this, {
570                 size: 'medium',
571                 title:result.warning.title,
572                 buttons: [
573                     {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
574                 ]
575             }, QWeb.render("CrashManager.warning", result.warning)).open();
576         }
577
578         return $.Deferred().resolve();
579         } catch(e) {
580             console.error(e);
581             instance.webclient.crashmanager.show_message(e);
582             return $.Deferred().reject();
583         }
584     },
585     _process_operations: function() {
586         var self = this;
587         return this.mutating_mutex.exec(function() {
588             function iterate() {
589                 var start = $.Deferred();
590                 start.resolve();
591                 start = _.reduce(self.onchanges_defs, function(memo, d){
592                     return memo.then(function(){
593                         return d;
594                     }, function(){
595                         return d;
596                     });
597                 }, start);
598                 var defs = [start];
599                 _.each(self.fields, function(field) {
600                     defs.push(field.commit_value());
601                 });
602                 var args = _.toArray(arguments);
603                 return $.when.apply($, defs).then(function() {
604                     var save_obj = self.save_list.pop();
605                     if (save_obj) {
606                         return self._process_save(save_obj).then(function() {
607                             save_obj.ret = _.toArray(arguments);
608                             return iterate();
609                         }, function() {
610                             save_obj.error = true;
611                         });
612                     }
613                     return $.when();
614                 }).fail(function() {
615                     self.save_list.pop();
616                     return $.when();
617                 });
618             }
619             return iterate();
620         });
621     },
622     _internal_set_values: function(values) {
623         for (var f in values) {
624             if (!values.hasOwnProperty(f)) { continue; }
625             var field = this.fields[f];
626             // If field is not defined in the view, just ignore it
627             if (field) {
628                 var value_ = values[f];
629                 if (field.get_value() != value_) {
630                     field._inhibit_on_change_flag = true;
631                     field.set_value(value_);
632                     field._inhibit_on_change_flag = false;
633                     field._dirty_flag = true;
634                 }
635             }
636         }
637         this.on_form_changed();
638     },
639     set_values: function(values) {
640         var self = this;
641         return this.mutating_mutex.exec(function() {
642             self._internal_set_values(values);
643         });
644     },
645     /**
646      * Ask the view to switch to view mode if possible. The view may not do it
647      * if the current record is not yet saved. It will then stay in create mode.
648      */
649     to_view_mode: function() {
650         this._actualize_mode("view");
651     },
652     /**
653      * Ask the view to switch to edit mode if possible. The view may not do it
654      * if the current record is not yet saved. It will then stay in create mode.
655      */
656     to_edit_mode: function() {
657         this.onchanges_defs = [];
658         this._actualize_mode("edit");
659     },
660     /**
661      * Ask the view to switch to a precise mode if possible. The view is free to
662      * not respect this command if the state of the dataset is not compatible with
663      * the new mode. For example, it is not possible to switch to edit mode if
664      * the current record is not yet saved in database.
665      *
666      * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
667      * undefined the view will test the actual mode to check if it is still consistent
668      * with the dataset state.
669      */
670     _actualize_mode: function(switch_to) {
671         var mode = switch_to || this.get("actual_mode");
672         if (! this.datarecord.id) {
673             mode = "create";
674         } else if (mode === "create") {
675             mode = "edit";
676         }
677         this.render_value_defs = [];
678         this.set({actual_mode: mode});
679     },
680     check_actual_mode: function(source, options) {
681         var self = this;
682         if(this.get("actual_mode") === "view") {
683             self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
684             self.$buttons.find('.oe_form_buttons_edit').hide();
685             self.$buttons.find('.oe_form_buttons_view').show();
686             self.$sidebar.show();
687         } else {
688             self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
689             self.$buttons.find('.oe_form_buttons_edit').show();
690             self.$buttons.find('.oe_form_buttons_view').hide();
691             self.$sidebar.hide();
692             this.autofocus();
693         }
694     },
695     autofocus: function() {
696         if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
697             var fields_order = this.fields_order.slice(0);
698             if (this.default_focus_field) {
699                 fields_order.unshift(this.default_focus_field.name);
700             }
701             for (var i = 0; i < fields_order.length; i += 1) {
702                 var field = this.fields[fields_order[i]];
703                 if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
704                     if (field.focus() !== false) {
705                         break;
706                     }
707                 }
708             }
709         }
710     },
711     on_button_save: function(e) {
712         var self = this;
713         $(e.target).attr("disabled", true);
714         return this.save().done(function(result) {
715             self.trigger("save", result);
716             self.reload().then(function() {
717                 self.to_view_mode();
718                 var menu = instance.webclient.menu;
719                 if (menu) {
720                     menu.do_reload_needaction();
721                 }
722                 instance.web.bus.trigger('form_view_saved', self);
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     start: function () {
2341         var self = this;
2342         this.states = [];
2343         this._super.apply(this, arguments);
2344         // hook on form view content changed: recompute the states, because it may be related to the current stage
2345         this.getParent().on('view_content_has_changed', self, function () {
2346             self.render_value();
2347         });
2348     },
2349     prepare_dropdown_selection: function() {
2350         var self = this;
2351         var data = [];
2352         var selection = this.field.selection || [];
2353         var stage_id = _.isArray(this.view.datarecord.stage_id) ? this.view.datarecord.stage_id[0] : this.view.datarecord.stage_id;
2354         var legend_field = this.options && this.options.states_legend_field || false;
2355         var fields_to_read = _.map(
2356             this.options && this.options.states_legend || {},
2357             function (value, key, list) { return value; });
2358         if (legend_field && fields_to_read && stage_id) {
2359             var fetch_stage = new openerp.web.DataSet(
2360                 this,
2361                 self.view.fields[legend_field].field.relation).read_ids([stage_id],
2362                 fields_to_read);
2363         }
2364         else { var fetch_stage = $.Deferred().resolve(false); }
2365         return $.when(fetch_stage).then(function (stage_data) {
2366             _.map(selection, function(res) {
2367                 var value = {
2368                     'name': res[0],
2369                     'tooltip': res[1],
2370                     'state_name': res[1],
2371                 }
2372                 if (stage_data && stage_data[0][self.options.states_legend[res[0]]]) {
2373                     value['state_name'] = stage_data[0][self.options.states_legend[res[0]]];
2374                 }
2375                 if (res[0] == 'normal') { value['state_class'] = 'oe_kanban_status'; }
2376                 else if (res[0] == 'done') { value['state_class'] = 'oe_kanban_status oe_kanban_status_green'; }
2377                 else { value['state_class'] = 'oe_kanban_status oe_kanban_status_red'; }
2378                 data.push(value);
2379             });
2380             return data;
2381         });
2382     },
2383     render_value: function() {
2384         var self = this;
2385         this.record_id = this.view.datarecord.id;
2386         var dd_fetched = this.prepare_dropdown_selection();;
2387         return $.when(dd_fetched).then(function (states) {
2388             self.states = states;
2389             self.$el.html(QWeb.render("KanbanSelection", {'widget': self}));
2390             self.$el.find('li').on('click', self.set_kanban_selection.bind(self));
2391         })
2392     },
2393     /* setting the value: in view mode, perform an asynchronous call and reload
2394     the form view; in edit mode, use set_value to save the new value that will
2395     be written when saving the record. */
2396     set_kanban_selection: function (ev) {
2397         var self = this;
2398         var li = $(ev.target).closest('li');
2399         if (li.length) {
2400             var value = String(li.data('value'));
2401             if (this.view.get('actual_mode') == 'view') {
2402                 var write_values = {}
2403                 write_values[self.name] = value;
2404                 return this.view.dataset._model.call(
2405                     'write', [
2406                         [self.record_id],
2407                         write_values,
2408                         self.view.dataset.get_context()
2409                     ]).done(self.reload_record.bind(self));
2410             }
2411             else {
2412                 return this.set_value(value);
2413             }
2414         }
2415     },
2416     reload_record: function() {
2417         this.view.reload();
2418     },
2419 });
2420
2421 instance.web.form.Priority = instance.web.form.FieldChar.extend({
2422     init: function (field_manager, node) {
2423         this._super(field_manager, node);
2424     },
2425     prepare_priority: function() {
2426         var self = this;
2427         var selection = this.field.selection || [];
2428         var init_value = selection && selection[0][0] || 0;
2429         var data = _.map(selection.slice(1), function(element, index) {
2430             var value = {
2431                 'value': element[0],
2432                 'name': element[1],
2433                 'click_value': element[0],
2434             }
2435             if (index == 0 && self.get('value') == element[0]) {
2436                 value['click_value'] = init_value;
2437             }
2438             return value;
2439         });
2440         return data;
2441     },
2442     render_value: function() {
2443         var self = this;
2444         this.record_id = this.view.datarecord.id;
2445         this.priorities = this.prepare_priority();
2446         this.$el.html(QWeb.render("Priority", {'widget': this}));
2447         this.$el.find('li').on('click', this.set_priority.bind(this));
2448     },
2449     /* setting the value: in view mode, perform an asynchronous call and reload
2450     the form view; in edit mode, use set_value to save the new value that will
2451     be written when saving the record. */
2452     set_priority: function (ev) {
2453         var self = this;
2454         var li = $(ev.target).closest('li');
2455         if (li.length) {
2456             var value = String(li.data('value'));
2457             if (this.view.get('actual_mode') == 'view') {
2458                 var write_values = {}
2459                 write_values[self.name] = value;
2460                 return this.view.dataset._model.call(
2461                     'write', [
2462                         [self.record_id],
2463                         write_values,
2464                         self.view.dataset.get_context()
2465                     ]).done(self.reload_record.bind(self));
2466             }
2467             else {
2468                 return this.set_value(value);
2469             }
2470         }
2471
2472     },
2473     reload_record: function() {
2474         this.view.reload();
2475     },
2476 });
2477
2478 instance.web.form.FieldID = instance.web.form.FieldChar.extend({
2479     process_modifiers: function () {
2480         this._super();
2481         this.set({ readonly: true });
2482     },
2483 });
2484
2485 instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
2486     template: 'FieldEmail',
2487     initialize_content: function() {
2488         this._super();
2489         var $button = this.$el.find('button');
2490         $button.click(this.on_button_clicked);
2491         this.setupFocus($button);
2492     },
2493     render_value: function() {
2494         if (!this.get("effective_readonly")) {
2495             this._super();
2496         } else {
2497             this.$el.find('a')
2498                     .attr('href', 'mailto:' + this.get('value'))
2499                     .text(this.get('value') || '');
2500         }
2501     },
2502     on_button_clicked: function() {
2503         if (!this.get('value') || !this.is_syntax_valid()) {
2504             this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
2505         } else {
2506             location.href = 'mailto:' + this.get('value');
2507         }
2508     }
2509 });
2510
2511 instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
2512     template: 'FieldUrl',
2513     initialize_content: function() {
2514         this._super();
2515         var $button = this.$el.find('button');
2516         $button.click(this.on_button_clicked);
2517         this.setupFocus($button);
2518     },
2519     render_value: function() {
2520         if (!this.get("effective_readonly")) {
2521             this._super();
2522         } else {
2523             var tmp = this.get('value');
2524             var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
2525             if (!s) {
2526                 tmp = "http://" + this.get('value');
2527             }
2528             var text = this.get('value') ? this.node.attrs.text || tmp : '';
2529             this.$el.find('a').attr('href', tmp).text(text);
2530         }
2531     },
2532     on_button_clicked: function() {
2533         if (!this.get('value')) {
2534             this.do_warn(_t("Resource Error"), _t("This resource is empty"));
2535         } else {
2536             var url = $.trim(this.get('value'));
2537             if(/^www\./i.test(url))
2538                 url = 'http://'+url;
2539             window.open(url);
2540         }
2541     }
2542 });
2543
2544 instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
2545     is_field_number: true,
2546     widget_class: 'oe_form_field_float',
2547     init: function (field_manager, node) {
2548         this._super(field_manager, node);
2549         this.internal_set_value(0);
2550         if (this.node.attrs.digits) {
2551             this.digits = this.node.attrs.digits;
2552         } else {
2553             this.digits = this.field.digits;
2554         }
2555     },
2556     set_value: function(value_) {
2557         if (value_ === false || value_ === undefined) {
2558             // As in GTK client, floats default to 0
2559             value_ = 0;
2560         }
2561         this._super.apply(this, [value_]);
2562     },
2563     focus: function () {
2564         var $input = this.$('input:first');
2565         return $input.length ? $input.select() : false;
2566     }
2567 });
2568
2569 instance.web.form.FieldCharDomain = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2570     init: function(field_manager, node) {
2571         this._super.apply(this, arguments);
2572     },
2573     start: function() {
2574         var self = this;
2575         this._super.apply(this, arguments);
2576         this.on("change:effective_readonly", this, function () {
2577             this.display_field();
2578         });
2579         this.display_field();
2580         return this._super();
2581     },
2582     set_value: function(value_) {
2583         var self = this;
2584         this.set('value', value_ || false);
2585         this.display_field();
2586      },
2587     display_field: function() {
2588         var self = this;
2589         this.$el.html(instance.web.qweb.render("FieldCharDomain", {widget: this}));
2590         if (this.get('value')) {
2591             var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2592             var domain = instance.web.pyeval.eval('domain', this.get('value'));
2593             var ds = new instance.web.DataSetStatic(self, model, self.build_context());
2594             ds.call('search_count', [domain]).then(function (results) {
2595                 $('.oe_domain_count', self.$el).text(results + ' records selected');
2596                 if (self.get('effective_readonly')) {
2597                     $('button span', self.$el).text(' See selection');
2598                 }
2599                 else {
2600                     $('button span', self.$el).text(' Change selection');
2601                 }
2602             });
2603         } else {
2604             $('.oe_domain_count', this.$el).text('0 record selected');
2605             $('button span', this.$el).text(' Select records');
2606         };
2607         this.$('.select_records').on('click', self.on_click);
2608     },
2609     on_click: function(event) {
2610         event.preventDefault();
2611         var self = this;
2612         var model = this.options.model || this.field_manager.get_field_value(this.options.model_field);
2613         this.pop = new instance.web.form.SelectCreatePopup(this);
2614         this.pop.select_element(
2615             model, {
2616                 title: this.get('effective_readonly') ? 'Selected records' : 'Select records...',
2617                 readonly: this.get('effective_readonly'),
2618                 disable_multiple_selection: this.get('effective_readonly'),
2619                 no_create: this.get('effective_readonly'),
2620             }, [], this.build_context());
2621         this.pop.on("elements_selected", self, function(element_ids) {
2622             if (this.pop.$('input.oe_list_record_selector').prop('checked')) {
2623                 var search_data = this.pop.searchview.build_search_data();
2624                 var domain_done = instance.web.pyeval.eval_domains_and_contexts({
2625                     domains: search_data.domains,
2626                     contexts: search_data.contexts,
2627                     group_by_seq: search_data.groupbys || []
2628                 }).then(function (results) {
2629                     return results.domain;
2630                 });
2631             }
2632             else {
2633                 var domain = [["id", "in", element_ids]];
2634                 var domain_done = $.Deferred().resolve(domain);
2635             }
2636             $.when(domain_done).then(function (domain) {
2637                 var domain = self.pop.dataset.domain.concat(domain || []);
2638                 self.set_value(domain);
2639             });
2640         });
2641     },
2642 });
2643
2644 instance.web.DateTimeWidget = instance.web.Widget.extend({
2645     template: "web.datepicker",
2646     type_of_date: "datetime",
2647     events: {
2648         'dp.change .oe_datepicker_main': 'change_datetime',
2649         'dp.show .oe_datepicker_main': 'set_datetime_default',
2650         'keypress .oe_datepicker_master': 'change_datetime',
2651     },
2652     init: function(parent) {
2653         this._super(parent);
2654         this.name = parent.name;
2655     },
2656     start: function() {
2657         var self = this;
2658         var l10n = _t.database.parameters;
2659         var options = {
2660             pickTime: true,
2661             useSeconds: true,
2662             startDate: moment({ y: 1900 }),
2663             endDate: moment().add(200, "y"),
2664             calendarWeeks: true,
2665             icons : {
2666                 time: 'fa fa-clock-o',
2667                 date: 'fa fa-calendar',
2668                 up: 'fa fa-chevron-up',
2669                 down: 'fa fa-chevron-down'
2670                },
2671             language : moment.locale(),
2672             format : instance.web.normalize_format(l10n.date_format +' '+ l10n.time_format),
2673         };
2674         this.$input = this.$el.find('input.oe_datepicker_master');
2675         if (this.type_of_date === 'date') {
2676             options['pickTime'] = false;
2677             options['useSeconds'] = false;
2678             options['format'] = instance.web.normalize_format(l10n.date_format);
2679         }
2680         this.picker = this.$('.oe_datepicker_main').datetimepicker(options);
2681         this.set_readonly(false);
2682         this.set({'value': false});
2683     },
2684     set_value: function(value_) {
2685         this.set({'value': value_});
2686         this.$input.val(value_ ? this.format_client(value_) : '');
2687     },
2688     get_value: function() {
2689         return this.get('value');
2690     },
2691     set_value_from_ui_: function() {
2692         var value_ = this.$input.val() || false;
2693         this.set_value(this.parse_client(value_));
2694     },
2695     set_readonly: function(readonly) {
2696         this.readonly = readonly;
2697         this.$input.prop('readonly', this.readonly);
2698     },
2699     is_valid_: function() {
2700         var value_ = this.$input.val();
2701         if (value_ === "") {
2702             return true;
2703         } else {
2704             try {
2705                 this.parse_client(value_);
2706                 return true;
2707             } catch(e) {
2708                 return false;
2709             }
2710         }
2711     },
2712     parse_client: function(v) {
2713         return instance.web.parse_value(v, {"widget": this.type_of_date});
2714     },
2715     format_client: function(v) {
2716         return instance.web.format_value(v, {"widget": this.type_of_date});
2717     },
2718     set_datetime_default: function(){
2719         //when opening datetimepicker the date and time by default should be the one from
2720         //the input field if any or the current day otherwise
2721         if (this.type_of_date === 'datetime') {
2722             value = moment().second(0);
2723             if (this.$input.val().length !== 0 && this.is_valid_()){
2724                 var value = this.$input.val();
2725             }
2726             this.$('.oe_datepicker_main').data('DateTimePicker').setValue(value);
2727         }
2728     },
2729     change_datetime: function(e) {
2730         if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) {
2731             this.set_value_from_ui_();
2732             this.trigger("datetime_changed");
2733         }
2734     },
2735     commit_value: function () {
2736         this.change_datetime();
2737     },
2738 });
2739
2740 instance.web.DateWidget = instance.web.DateTimeWidget.extend({
2741     type_of_date: "date"
2742 });
2743
2744 instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2745     template: "FieldDatetime",
2746     build_widget: function() {
2747         return new instance.web.DateTimeWidget(this);
2748     },
2749     destroy_content: function() {
2750         if (this.datewidget) {
2751             this.datewidget.destroy();
2752             this.datewidget = undefined;
2753         }
2754     },
2755     initialize_content: function() {
2756         if (!this.get("effective_readonly")) {
2757             this.datewidget = this.build_widget();
2758             this.datewidget.on('datetime_changed', this, _.bind(function() {
2759                 this.internal_set_value(this.datewidget.get_value());
2760             }, this));
2761             this.datewidget.appendTo(this.$el);
2762             this.setupFocus(this.datewidget.$input);
2763         }
2764     },
2765     render_value: function() {
2766         if (!this.get("effective_readonly")) {
2767             this.datewidget.set_value(this.get('value'));
2768         } else {
2769             this.$el.text(instance.web.format_value(this.get('value'), this, ''));
2770         }
2771     },
2772     is_syntax_valid: function() {
2773         if (!this.get("effective_readonly") && this.datewidget) {
2774             return this.datewidget.is_valid_();
2775         }
2776         return true;
2777     },
2778     is_false: function() {
2779         return this.get('value') === '' || this._super();
2780     },
2781     focus: function() {
2782         var input = this.datewidget && this.datewidget.$input[0];
2783         return input ? input.focus() : false;
2784     },
2785     set_dimensions: function (height, width) {
2786         this._super(height, width);
2787         if (!this.get("effective_readonly")) {
2788             this.datewidget.$input.css('height', height);
2789         }
2790     }
2791 });
2792
2793 instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
2794     template: "FieldDate",
2795     build_widget: function() {
2796         return new instance.web.DateWidget(this);
2797     }
2798 });
2799
2800 instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2801     template: 'FieldText',
2802     events: {
2803         'keyup': function (e) {
2804             if (e.which === $.ui.keyCode.ENTER) {
2805                 e.stopPropagation();
2806             }
2807         },
2808         'keypress': function (e) {
2809             if (e.which === $.ui.keyCode.ENTER) {
2810                 e.stopPropagation();
2811             }
2812         },
2813         'change textarea': 'store_dom_value',
2814     },
2815     initialize_content: function() {
2816         var self = this;
2817         if (! this.get("effective_readonly")) {
2818             this.$textarea = this.$el.find('textarea');
2819             this.auto_sized = false;
2820             this.default_height = this.$textarea.css('height');
2821             if (this.get("effective_readonly")) {
2822                 this.$textarea.attr('disabled', 'disabled');
2823             }
2824             this.setupFocus(this.$textarea);
2825         } else {
2826             this.$textarea = undefined;
2827         }
2828     },
2829     commit_value: function () {
2830         if (! this.get("effective_readonly") && this.$textarea) {
2831             this.store_dom_value();
2832         }
2833         return this._super();
2834     },
2835     store_dom_value: function () {
2836         this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
2837     },
2838     render_value: function() {
2839         if (! this.get("effective_readonly")) {
2840             var show_value = instance.web.format_value(this.get('value'), this, '');
2841             if (show_value === '') {
2842                 this.$textarea.css('height', parseInt(this.default_height, 10)+"px");
2843             }
2844             this.$textarea.val(show_value);
2845             if (! this.auto_sized) {
2846                 this.auto_sized = true;
2847                 this.$textarea.autosize();
2848             } else {
2849                 this.$textarea.trigger("autosize");
2850             }
2851         } else {
2852             var txt = this.get("value") || '';
2853             this.$(".oe_form_text_content").text(txt);
2854         }
2855     },
2856     is_syntax_valid: function() {
2857         if (!this.get("effective_readonly") && this.$textarea) {
2858             try {
2859                 instance.web.parse_value(this.$textarea.val(), this, '');
2860                 return true;
2861             } catch(e) {
2862                 return false;
2863             }
2864         }
2865         return true;
2866     },
2867     is_false: function() {
2868         return this.get('value') === '' || this._super();
2869     },
2870     focus: function($el) {
2871         var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
2872         return input ? input.focus() : false;
2873     },
2874     set_dimensions: function (height, width) {
2875         this._super(height, width);
2876         if (!this.get("effective_readonly") && this.$textarea) {
2877             this.$textarea.css({
2878                 width: width,
2879                 minHeight: height
2880             });
2881         }
2882     },
2883 });
2884
2885 /**
2886  * FieldTextHtml Widget
2887  * Intended for FieldText widgets meant to display HTML content. This
2888  * widget will instantiate the CLEditor (see cleditor in static/src/lib)
2889  * To find more information about CLEditor configutation: go to
2890  * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
2891  */
2892 instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2893     template: 'FieldTextHtml',
2894     init: function() {
2895         this._super.apply(this, arguments);
2896     },
2897     initialize_content: function() {
2898         var self = this;
2899         if (! this.get("effective_readonly")) {
2900             self._updating_editor = false;
2901             this.$textarea = this.$el.find('textarea');
2902             var width = ((this.node.attrs || {}).editor_width || 'calc(100% - 4px)');
2903             var height = ((this.node.attrs || {}).editor_height || 250);
2904             this.$textarea.cleditor({
2905                 width:      width, // width not including margins, borders or padding
2906                 height:     height, // height not including margins, borders or padding
2907                 controls:   // controls to add to the toolbar
2908                             "bold italic underline strikethrough " +
2909                             "| removeformat | bullets numbering | outdent " +
2910                             "indent | link unlink | source",
2911                 bodyStyle:  // style to assign to document body contained within the editor
2912                             "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
2913             });
2914             this.$cleditor = this.$textarea.cleditor()[0];
2915             this.$cleditor.change(function() {
2916                 if (! self._updating_editor) {
2917                     self.$cleditor.updateTextArea();
2918                     self.internal_set_value(self.$textarea.val());
2919                 }
2920             });
2921             if (this.field.translate) {
2922                 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"/>')
2923                     .click(this.on_translate);
2924                 this.$cleditor.$toolbar.append($img);
2925             }
2926         }
2927     },
2928     render_value: function() {
2929         if (! this.get("effective_readonly")) {
2930             this.$textarea.val(this.get('value') || '');
2931             this._updating_editor = true;
2932             this.$cleditor.updateFrame();
2933             this._updating_editor = false;
2934         } else {
2935             this.$el.html(this.get('value'));
2936         }
2937     },
2938 });
2939
2940 instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
2941     template: 'FieldBoolean',
2942     start: function() {
2943         var self = this;
2944         this.$checkbox = $("input", this.$el);
2945         this.setupFocus(this.$checkbox);
2946         this.$el.click(_.bind(function() {
2947             this.internal_set_value(this.$checkbox.is(':checked'));
2948         }, this));
2949         var check_readonly = function() {
2950             self.$checkbox.prop('disabled', self.get("effective_readonly"));
2951             self.click_disabled_boolean();
2952         };
2953         this.on("change:effective_readonly", this, check_readonly);
2954         check_readonly.call(this);
2955         this._super.apply(this, arguments);
2956     },
2957     render_value: function() {
2958         this.$checkbox[0].checked = this.get('value');
2959     },
2960     focus: function() {
2961         var input = this.$checkbox && this.$checkbox[0];
2962         return input ? input.focus() : false;
2963     },
2964     click_disabled_boolean: function(){
2965         var $disabled = this.$el.find('input[type=checkbox]:disabled');
2966         $disabled.each(function (){
2967             $(this).next('div').remove();
2968             $(this).closest("span").append($('<div class="boolean"></div>'));
2969         });
2970     }
2971 });
2972
2973 /**
2974     The progressbar field expect a float from 0 to 100.
2975 */
2976 instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
2977     template: 'FieldProgressBar',
2978     render_value: function() {
2979         this.$el.progressbar({
2980             value: this.get('value') || 0,
2981             disabled: this.get("effective_readonly")
2982         });
2983         var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
2984         this.$('span').html(formatted_value + '%');
2985     }
2986 });
2987
2988 /**
2989     The PercentPie field expect a float from 0 to 100.
2990 */
2991 instance.web.form.FieldPercentPie = instance.web.form.AbstractField.extend({
2992     template: 'FieldPercentPie',
2993
2994     render_value: function() {
2995         var value = this.get('value'),
2996             formatted_value = Math.round(value || 0) + '%',
2997             svg = this.$('svg')[0];
2998
2999         svg.innerHTML = "";
3000         nv.addGraph(function() {
3001             var width = 42, height = 42;
3002             var chart = nv.models.pieChart()
3003                 .width(width)
3004                 .height(height)
3005                 .margin({top: 0, right: 0, bottom: 0, left: 0})
3006                 .donut(true) 
3007                 .showLegend(false)
3008                 .showLabels(false)
3009                 .tooltips(false)
3010                 .color(['#7C7BAD','#DDD'])
3011                 .donutRatio(0.62);
3012    
3013             d3.select(svg)
3014                 .datum([{'x': 'value', 'y': value}, {'x': 'complement', 'y': 100 - value}])
3015                 .transition()
3016                 .call(chart)
3017                 .attr('style', 'width: ' + width + 'px; height:' + height + 'px;');
3018
3019             d3.select(svg)
3020                 .append("text")
3021                 .attr({x: width/2, y: height/2 + 3, 'text-anchor': 'middle'})
3022                 .style({"font-size": "10px", "font-weight": "bold"})
3023                 .text(formatted_value);
3024
3025             return chart;
3026         });
3027    
3028     }
3029 });
3030
3031 /**
3032     The FieldBarChart expectsa list of values (indeed)
3033 */
3034 instance.web.form.FieldBarChart = instance.web.form.AbstractField.extend({
3035     template: 'FieldBarChart',
3036
3037     render_value: function() {
3038         var value = JSON.parse(this.get('value'));
3039         var svg = this.$('svg')[0];
3040         svg.innerHTML = "";
3041         nv.addGraph(function() {
3042             var width = 34, height = 34;
3043             var chart = nv.models.discreteBarChart()
3044                 .x(function (d) { return d.tooltip })
3045                 .y(function (d) { return d.value })
3046                 .width(width)
3047                 .height(height)
3048                 .margin({top: 0, right: 0, bottom: 0, left: 0})
3049                 .tooltips(false)
3050                 .showValues(false)
3051                 .transitionDuration(350)
3052                 .showXAxis(false)
3053                 .showYAxis(false);
3054    
3055             d3.select(svg)
3056                 .datum([{key: 'values', values: value}])
3057                 .transition()
3058                 .call(chart)
3059                 .attr('style', 'width: ' + (width + 4) + 'px; height: ' + (height + 8) + 'px;');
3060
3061             nv.utils.windowResize(chart.update);
3062
3063             return chart;
3064         });
3065    
3066     }
3067 });
3068
3069
3070 instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3071     template: 'FieldSelection',
3072     events: {
3073         'change select': 'store_dom_value',
3074     },
3075     init: function(field_manager, node) {
3076         var self = this;
3077         this._super(field_manager, node);
3078         this.set("value", false);
3079         this.set("values", []);
3080         this.records_orderer = new instance.web.DropMisordered();
3081         this.field_manager.on("view_content_has_changed", this, function() {
3082             var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
3083             if (! _.isEqual(domain, this.get("domain"))) {
3084                 this.set("domain", domain);
3085             }
3086         });
3087     },
3088     initialize_field: function() {
3089         instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3090         this.on("change:domain", this, this.query_values);
3091         this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
3092         this.on("change:values", this, this.render_value);
3093     },
3094     query_values: function() {
3095         var self = this;
3096         var def;
3097         if (this.field.type === "many2one") {
3098             var model = new openerp.Model(openerp.session, this.field.relation);
3099             def = model.call("name_search", ['', this.get("domain")], {"context": this.build_context()});
3100         } else {
3101             var values = _.reject(this.field.selection, function (v) { return v[0] === false && v[1] === ''; });
3102             def = $.when(values);
3103         }
3104         this.records_orderer.add(def).then(function(values) {
3105             if (! _.isEqual(values, self.get("values"))) {
3106                 self.set("values", values);
3107             }
3108         });
3109     },
3110     initialize_content: function() {
3111         // Flag indicating whether we're in an event chain containing a change
3112         // event on the select, in order to know what to do on keyup[RETURN]:
3113         // * If the user presses [RETURN] as part of changing the value of a
3114         //   selection, we should just let the value change and not let the
3115         //   event broadcast further (e.g. to validating the current state of
3116         //   the form in editable list view, which would lead to saving the
3117         //   current row or switching to the next one)
3118         // * If the user presses [RETURN] with a select closed (side-effect:
3119         //   also if the user opened the select and pressed [RETURN] without
3120         //   changing the selected value), takes the action as validating the
3121         //   row
3122         var ischanging = false;
3123         var $select = this.$el.find('select')
3124             .change(function () { ischanging = true; })
3125             .click(function () { ischanging = false; })
3126             .keyup(function (e) {
3127                 if (e.which !== 13 || !ischanging) { return; }
3128                 e.stopPropagation();
3129                 ischanging = false;
3130             });
3131         this.setupFocus($select);
3132     },
3133     commit_value: function () {
3134         this.store_dom_value();
3135         return this._super();
3136     },
3137     store_dom_value: function () {
3138         if (!this.get('effective_readonly') && this.$('select').length) {
3139             var val = JSON.parse(this.$('select').val());
3140             this.internal_set_value(val);
3141         }
3142     },
3143     set_value: function(value_) {
3144         value_ = value_ === null ? false : value_;
3145         value_ = value_ instanceof Array ? value_[0] : value_;
3146         this._super(value_);
3147     },
3148     render_value: function() {
3149         var values = this.get("values");
3150         values =  [[false, this.node.attrs.placeholder || '']].concat(values);
3151         var found = _.find(values, function(el) { return el[0] === this.get("value"); }, this);
3152         if (! found) {
3153             found = [this.get("value"), _t('Unknown')];
3154             values = [found].concat(values);
3155         }
3156         if (! this.get("effective_readonly")) {
3157             this.$().html(QWeb.render("FieldSelectionSelect", {widget: this, values: values}));
3158             this.$("select").val(JSON.stringify(found[0]));
3159         } else {
3160             this.$el.text(found[1]);
3161         }
3162     },
3163     focus: function() {
3164         var input = this.$('select:first')[0];
3165         return input ? input.focus() : false;
3166     },
3167     set_dimensions: function (height, width) {
3168         this._super(height, width);
3169         this.$('select').css({
3170             height: height,
3171             width: width
3172         });
3173     }
3174 });
3175
3176 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
3177     template: 'FieldRadio',
3178     events: {
3179         'click input': 'click_change_value'
3180     },
3181     init: function(field_manager, node) {
3182         /* Radio button widget: Attributes options:
3183         * - "horizontal" to display in column
3184         * - "no_radiolabel" don't display text values
3185         */
3186         this._super(field_manager, node);
3187         this.selection = _.clone(this.field.selection) || [];
3188         this.domain = false;
3189         this.uniqueId = _.uniqueId("radio");
3190     },
3191     initialize_content: function () {
3192         this.on("change:effective_readonly", this, this.render_value);
3193         this.field_manager.on("view_content_has_changed", this, this.get_selection);
3194         this.get_selection();
3195     },
3196     click_change_value: function (event) {
3197         var val = $(event.target).val();
3198         val = this.field.type == "selection" ? val : +val;
3199         if (val == this.get_value()) {
3200             this.set_value(false);
3201         } else {
3202             this.set_value(val);
3203         }
3204     },
3205     /** Get the selection and render it
3206      *  selection: [[identifier, value_to_display], ...]
3207      *  For selection fields: this is directly given by this.field.selection
3208      *  For many2one fields:  perform a search on the relation of the many2one field
3209      */
3210     get_selection: function() {
3211         var self = this;
3212         var selection = [];
3213         var def = $.Deferred();
3214         if (self.field.type == "many2one") {
3215             var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
3216             if (! _.isEqual(self.domain, domain)) {
3217                 self.domain = domain;
3218                 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
3219                 ds.call('search', [self.domain])
3220                     .then(function (records) {
3221                         ds.name_get(records).then(function (records) {
3222                             selection = records;
3223                             def.resolve();
3224                         });
3225                     });
3226             } else {
3227                 selection = self.selection;
3228                 def.resolve();
3229             }
3230         }
3231         else if (self.field.type == "selection") {
3232             selection = self.field.selection || [];
3233             def.resolve();
3234         }
3235         return def.then(function () {
3236             if (! _.isEqual(selection, self.selection)) {
3237                 self.selection = _.clone(selection);
3238                 self.renderElement();
3239                 self.render_value();
3240             }
3241         });
3242     },
3243     set_value: function (value_) {
3244         if (value_) {
3245             if (this.field.type == "selection") {
3246                 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_;});
3247             }
3248             else if (!this.selection.length) {
3249                 this.selection = [value_];
3250             }
3251         }
3252         this._super(value_);
3253     },
3254     get_value: function () {
3255         var value = this.get('value');
3256         return value instanceof Array ? value[0] : value;
3257     },
3258     render_value: function () {
3259         var self = this;
3260         this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
3261         this.$("input:checked").prop("checked", false);
3262         if (this.get_value()) {
3263             this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true);
3264             this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
3265         }
3266     }
3267 });
3268
3269 // jquery autocomplete tweak to allow html and classnames
3270 (function() {
3271     var proto = $.ui.autocomplete.prototype,
3272         initSource = proto._initSource;
3273
3274     function filter( array, term ) {
3275         var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
3276         return $.grep( array, function(value_) {
3277             return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
3278         });
3279     }
3280
3281     $.extend( proto, {
3282         _initSource: function() {
3283             if ( this.options.html && $.isArray(this.options.source) ) {
3284                 this.source = function( request, response ) {
3285                     response( filter( this.options.source, request.term ) );
3286                 };
3287             } else {
3288                 initSource.call( this );
3289             }
3290         },
3291
3292         _renderItem: function( ul, item) {
3293             return $( "<li></li>" )
3294                 .data( "item.autocomplete", item )
3295                 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
3296                 .appendTo( ul )
3297                 .addClass(item.classname);
3298         }
3299     });
3300 })();
3301
3302 /**
3303     A mixin containing some useful methods to handle completion inputs.
3304     
3305     The widget containing this option can have these arguments in its widget options:
3306     - no_quick_create: if true, it will disable the quick create
3307 */
3308 instance.web.form.CompletionFieldMixin = {
3309     init: function() {
3310         this.limit = 7;
3311         this.orderer = new instance.web.DropMisordered();
3312     },
3313     /**
3314      * Call this method to search using a string.
3315      */
3316     get_search_result: function(search_val) {
3317         var self = this;
3318
3319         var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3320         this.last_query = search_val;
3321         var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
3322         if (!_(ids_blacklist).isEmpty()) {
3323             exclusion_domain.push(['id', 'not in', ids_blacklist]);
3324         }
3325
3326         return this.orderer.add(dataset.name_search(
3327                 search_val, new instance.web.CompoundDomain(self.build_domain(), exclusion_domain),
3328                 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3329             self.last_search = data;
3330             // possible selections for the m2o
3331             var values = _.map(data, function(x) {
3332                 x[1] = x[1].split("\n")[0];
3333                 return {
3334                     label: _.str.escapeHTML(x[1]),
3335                     value: x[1],
3336                     name: x[1],
3337                     id: x[0],
3338                 };
3339             });
3340
3341             // search more... if more results that max
3342             if (values.length > self.limit) {
3343                 values = values.slice(0, self.limit);
3344                 values.push({
3345                     label: _t("Search More..."),
3346                     action: function() {
3347                         dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3348                             self._search_create_popup("search", data);
3349                         });
3350                     },
3351                     classname: 'oe_m2o_dropdown_option'
3352                 });
3353             }
3354             // quick create
3355             var raw_result = _(data.result).map(function(x) {return x[1];});
3356             if (search_val.length > 0 && !_.include(raw_result, search_val) &&
3357                 ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3358                 values.push({
3359                     label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3360                         $('<span />').text(search_val).html()),
3361                     action: function() {
3362                         self._quick_create(search_val);
3363                     },
3364                     classname: 'oe_m2o_dropdown_option'
3365                 });
3366             }
3367             // create...
3368             if (!(self.options && (self.options.no_create || self.options.no_create_edit))){
3369                 values.push({
3370                     label: _t("Create and Edit..."),
3371                     action: function() {
3372                         self._search_create_popup("form", undefined, self._create_context(search_val));
3373                     },
3374                     classname: 'oe_m2o_dropdown_option'
3375                 });
3376             }
3377             else if (values.length == 0)
3378                 values.push({
3379                         label: _t("No results to show..."),
3380                         action: function() {},
3381                         classname: 'oe_m2o_dropdown_option'
3382                 });
3383
3384             return values;
3385         });
3386     },
3387     get_search_blacklist: function() {
3388         return [];
3389     },
3390     _quick_create: function(name) {
3391         var self = this;
3392         var slow_create = function () {
3393             self._search_create_popup("form", undefined, self._create_context(name));
3394         };
3395         if (self.options.quick_create === undefined || self.options.quick_create) {
3396             new instance.web.DataSet(this, this.field.relation, self.build_context())
3397                 .name_create(name).done(function(data) {
3398                     if (!self.get('effective_readonly'))
3399                         self.add_id(data[0]);
3400                 }).fail(function(error, event) {
3401                     event.preventDefault();
3402                     slow_create();
3403                 });
3404         } else
3405             slow_create();
3406     },
3407     // all search/create popup handling
3408     _search_create_popup: function(view, ids, context) {
3409         var self = this;
3410         var pop = new instance.web.form.SelectCreatePopup(this);
3411         pop.select_element(
3412             self.field.relation,
3413             {
3414                 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3415                 initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
3416                 initial_view: view,
3417                 disable_multiple_selection: true
3418             },
3419             self.build_domain(),
3420             new instance.web.CompoundContext(self.build_context(), context || {})
3421         );
3422         pop.on("elements_selected", self, function(element_ids) {
3423             self.add_id(element_ids[0]);
3424             self.focus();
3425         });
3426     },
3427     /**
3428      * To implement.
3429      */
3430     add_id: function(id) {},
3431     _create_context: function(name) {
3432         var tmp = {};
3433         var field = (this.options || {}).create_name_field;
3434         if (field === undefined)
3435             field = "name";
3436         if (field !== false && name && (this.options || {}).quick_create !== false)
3437             tmp["default_" + field] = name;
3438         return tmp;
3439     },
3440 };
3441
3442 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3443     template: "M2ODialog",
3444     init: function(parent) {
3445         this.name = parent.string;
3446         this._super(parent, {
3447             title: _.str.sprintf(_t("Create a %s"), parent.string),
3448             size: 'medium',
3449         });
3450     },
3451     start: function() {
3452         var self = this;
3453         var text = _.str.sprintf(_t("You are creating a new %s, are you sure it does not exist yet?"), self.name);
3454         this.$("p").text( text );
3455         this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3456         this.$("input").val(this.getParent().last_query);
3457         this.$buttons.find(".oe_form_m2o_qc_button").click(function(e){
3458             if (self.$("input").val() != ''){
3459                 self.getParent()._quick_create(self.$("input").val());
3460                 self.destroy();
3461             } else{
3462                 e.preventDefault();
3463                 self.$("input").focus();
3464             }
3465         });
3466         this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3467             self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3468             self.destroy();
3469         });
3470         this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3471             self.destroy();
3472         });
3473     },
3474 });
3475
3476 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3477     template: "FieldMany2One",
3478     events: {
3479         'keydown input': function (e) {
3480             switch (e.which) {
3481             case $.ui.keyCode.UP:
3482             case $.ui.keyCode.DOWN:
3483                 e.stopPropagation();
3484             }
3485         },
3486     },
3487     init: function(field_manager, node) {
3488         this._super(field_manager, node);
3489         instance.web.form.CompletionFieldMixin.init.call(this);
3490         this.set({'value': false});
3491         this.display_value = {};
3492         this.display_value_backup = {};
3493         this.last_search = [];
3494         this.floating = false;
3495         this.current_display = null;
3496         this.is_started = false;
3497         this.ignore_focusout = false;
3498     },
3499     reinit_value: function(val) {
3500         this.internal_set_value(val);
3501         this.floating = false;
3502         if (this.is_started)
3503             this.render_value();
3504     },
3505     initialize_field: function() {
3506         this.is_started = true;
3507         instance.web.bus.on('click', this, function() {
3508             if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3509                 this.$input.autocomplete("close");
3510             }
3511         });
3512         instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3513     },
3514     initialize_content: function() {
3515         if (!this.get("effective_readonly"))
3516             this.render_editable();
3517     },
3518     destroy_content: function () {
3519         if (this.$drop_down) {
3520             this.$drop_down.off('click');
3521             delete this.$drop_down;
3522         }
3523         if (this.$input) {
3524             this.$input.closest(".modal .modal-content").off('scroll');
3525             this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3526                             'focus focusout change keydown');
3527             delete this.$input;
3528         }
3529         if (this.$follow_button) {
3530             this.$follow_button.off('blur focus click');
3531             delete this.$follow_button;
3532         }
3533     },
3534     destroy: function () {
3535         this.destroy_content();
3536         return this._super();
3537     },
3538     init_error_displayer: function() {
3539         // nothing
3540     },
3541     hide_error_displayer: function() {
3542         // doesn't work
3543     },
3544     show_error_displayer: function() {
3545         new instance.web.form.M2ODialog(this).open();
3546     },
3547     render_editable: function() {
3548         var self = this;
3549         this.$input = this.$el.find("input");
3550
3551         this.init_error_displayer();
3552
3553         self.$input.on('focus', function() {
3554             self.hide_error_displayer();
3555         });
3556
3557         this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3558         this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3559
3560         this.$follow_button.click(function(ev) {
3561             ev.preventDefault();
3562             if (!self.get('value')) {
3563                 self.focus();
3564                 return;
3565             }
3566             var pop = new instance.web.form.FormOpenPopup(self);
3567             var context = self.build_context().eval();
3568             var model_obj = new instance.web.Model(self.field.relation);
3569             model_obj.call('get_formview_id', [self.get("value"), context]).then(function(view_id){
3570                 pop.show_element(
3571                     self.field.relation,
3572                     self.get("value"),
3573                     self.build_context(),
3574                     {
3575                         title: _t("Open: ") + self.string,
3576                         view_id: view_id
3577                     }
3578                 );
3579                 pop.on('write_completed', self, function(){
3580                     self.display_value = {};
3581                     self.display_value_backup = {};
3582                     self.render_value();
3583                     self.focus();
3584                     self.trigger('changed_value');
3585                 });
3586             });
3587         });
3588
3589         // some behavior for input
3590         var input_changed = function() {
3591             if (self.current_display !== self.$input.val()) {
3592                 self.current_display = self.$input.val();
3593                 if (self.$input.val() === "") {
3594                     self.internal_set_value(false);
3595                     self.floating = false;
3596                 } else {
3597                     self.floating = true;
3598                 }
3599             }
3600         };
3601         this.$input.keydown(input_changed);
3602         this.$input.change(input_changed);
3603         this.$drop_down.click(function() {
3604             self.$input.focus();
3605             if (self.$input.autocomplete("widget").is(":visible")) {
3606                 self.$input.autocomplete("close");                
3607             } else {
3608                 if (self.get("value") && ! self.floating) {
3609                     self.$input.autocomplete("search", "");
3610                 } else {
3611                     self.$input.autocomplete("search");
3612                 }
3613             }
3614         });
3615
3616         // Autocomplete close on dialog content scroll
3617         var close_autocomplete = _.debounce(function() {
3618             if (self.$input.autocomplete("widget").is(":visible")) {
3619                 self.$input.autocomplete("close");
3620             }
3621         }, 50);
3622         this.$input.closest(".modal .modal-content").on('scroll', this, close_autocomplete);
3623
3624         self.ed_def = $.Deferred();
3625         self.uned_def = $.Deferred();
3626         var ed_delay = 200;
3627         var ed_duration = 15000;
3628         var anyoneLoosesFocus = function (e) {
3629             if (self.ignore_focusout) { return; }
3630             var used = false;
3631             if (self.floating) {
3632                 if (self.last_search.length > 0) {
3633                     if (self.last_search[0][0] != self.get("value")) {
3634                         self.display_value = {};
3635                         self.display_value_backup = {};
3636                         self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3637                         self.reinit_value(self.last_search[0][0]);
3638                     } else {
3639                         used = true;
3640                         self.render_value();
3641                     }
3642                 } else {
3643                     used = true;
3644                     self.reinit_value(false);
3645                 }
3646                 self.floating = false;
3647             }
3648             if (used && self.get("value") === false && ! self.no_ed && ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
3649                 self.ed_def.reject();
3650                 self.uned_def.reject();
3651                 self.ed_def = $.Deferred();
3652                 self.ed_def.done(function() {
3653                     self.show_error_displayer();
3654                     ignore_blur = false;
3655                     self.trigger('focused');
3656                 });
3657                 ignore_blur = true;
3658                 setTimeout(function() {
3659                     self.ed_def.resolve();
3660                     self.uned_def.reject();
3661                     self.uned_def = $.Deferred();
3662                     self.uned_def.done(function() {
3663                         self.hide_error_displayer();
3664                     });
3665                     setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3666                 }, ed_delay);
3667             } else {
3668                 self.no_ed = false;
3669                 self.ed_def.reject();
3670             }
3671         };
3672         var ignore_blur = false;
3673         this.$input.on({
3674             focusout: anyoneLoosesFocus,
3675             focus: function () { self.trigger('focused'); },
3676             autocompleteopen: function () { ignore_blur = true; },
3677             autocompleteclose: function () { setTimeout(function() {ignore_blur = false;},0); },
3678             blur: function () {
3679                 // autocomplete open
3680                 if (ignore_blur) { $(this).focus(); return; }
3681                 if (_(self.getChildren()).any(function (child) {
3682                     return child instanceof instance.web.form.AbstractFormPopup;
3683                 })) { return; }
3684                 self.trigger('blurred');
3685             }
3686         });
3687
3688         var isSelecting = false;
3689         // autocomplete
3690         this.$input.autocomplete({
3691             source: function(req, resp) {
3692                 self.get_search_result(req.term).done(function(result) {
3693                     resp(result);
3694                 });
3695             },
3696             select: function(event, ui) {
3697                 isSelecting = true;
3698                 var item = ui.item;
3699                 if (item.id) {
3700                     self.display_value = {};
3701                     self.display_value_backup = {};
3702                     self.display_value["" + item.id] = item.name;
3703                     self.reinit_value(item.id);
3704                 } else if (item.action) {
3705                     item.action();
3706                     // Cancel widget blurring, to avoid form blur event
3707                     self.trigger('focused');
3708                     return false;
3709                 }
3710             },
3711             focus: function(e, ui) {
3712                 e.preventDefault();
3713             },
3714             html: true,
3715             // disabled to solve a bug, but may cause others
3716             //close: anyoneLoosesFocus,
3717             minLength: 0,
3718             delay: 250
3719         });
3720         // set position for list of suggestions box
3721         this.$input.autocomplete( "option", "position", { my : "left top", at: "left bottom" } );
3722         this.$input.autocomplete("widget").openerpClass();
3723         // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3724         this.$input.keyup(function(e) {
3725             if (e.which === 13) { // ENTER
3726                 if (isSelecting)
3727                     e.stopPropagation();
3728             }
3729             isSelecting = false;
3730         });
3731         this.setupFocus(this.$follow_button);
3732     },
3733     render_value: function(no_recurse) {
3734         var self = this;
3735         if (! this.get("value")) {
3736             this.display_string("");
3737             return;
3738         }
3739         var display = this.display_value["" + this.get("value")];
3740         if (display) {
3741             this.display_string(display);
3742             return;
3743         }
3744         if (! no_recurse) {
3745             var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3746             this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3747                 if (!data[0]) {
3748                     self.do_warn(_t("Render"), _t("No value found for the field "+self.field.string+" for value "+self.get("value")));
3749                     return;
3750                 }
3751                 self.display_value["" + self.get("value")] = data[0][1];
3752                 self.render_value(true);
3753             }).fail( function (data, event) {
3754                 // avoid displaying crash errors as many2One should be name_get compliant
3755                 event.preventDefault();
3756                 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3757                 self.render_value(true);
3758             });
3759         }
3760     },
3761     display_string: function(str) {
3762         var self = this;
3763         if (!this.get("effective_readonly")) {
3764             this.$input.val(str.split("\n")[0]);
3765             this.current_display = this.$input.val();
3766             if (this.is_false()) {
3767                 this.$('.oe_m2o_cm_button').css({'display':'none'});
3768             } else {
3769                 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3770             }
3771         } else {
3772             var lines = _.escape(str).split("\n");
3773             var link = "";
3774             var follow = "";
3775             link = lines[0];
3776             follow = _.rest(lines).join("<br />");
3777             if (follow)
3778                 link += "<br />";
3779             var $link = this.$el.find('.oe_form_uri')
3780                  .unbind('click')
3781                  .html(link);
3782             if (! this.options.no_open)
3783                 $link.click(function () {
3784                     var context = self.build_context().eval();
3785                     var model_obj = new instance.web.Model(self.field.relation);
3786                     model_obj.call('get_formview_action', [self.get("value"), context]).then(function(action){
3787                         self.do_action(action);
3788                     });
3789                     return false;
3790                  });
3791             $(".oe_form_m2o_follow", this.$el).html(follow);
3792         }
3793     },
3794     set_value: function(value_) {
3795         var self = this;
3796         if (value_ instanceof Array) {
3797             this.display_value = {};
3798             this.display_value_backup = {};
3799             if (! this.options.always_reload) {
3800                 this.display_value["" + value_[0]] = value_[1];
3801             }
3802             else {
3803                 this.display_value_backup["" + value_[0]] = value_[1];
3804             }
3805             value_ = value_[0];
3806         }
3807         value_ = value_ || false;
3808         this.reinit_value(value_);
3809     },
3810     get_displayed: function() {
3811         return this.display_value["" + this.get("value")];
3812     },
3813     add_id: function(id) {
3814         this.display_value = {};
3815         this.display_value_backup = {};
3816         this.reinit_value(id);
3817     },
3818     is_false: function() {
3819         return ! this.get("value");
3820     },
3821     focus: function () {
3822         var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3823         return input ? input.focus() : false;
3824     },
3825     _quick_create: function() {
3826         this.no_ed = true;
3827         this.ed_def.reject();
3828         return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3829     },
3830     _search_create_popup: function() {
3831         this.no_ed = true;
3832         this.ed_def.reject();
3833         this.ignore_focusout = true;
3834         this.reinit_value(false);
3835         var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3836         this.ignore_focusout = false;
3837         this.no_ed = false;
3838         return res;
3839     },
3840     set_dimensions: function (height, width) {
3841         this._super(height, width);
3842         if (!this.get("effective_readonly") && this.$input)
3843             this.$input.css('height', height);
3844     }
3845 });
3846
3847 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3848     template: 'Many2OneButton',
3849     init: function(field_manager, node) {
3850         this._super.apply(this, arguments);
3851     },
3852     start: function() {
3853         this._super.apply(this, arguments);
3854         this.set_button();
3855     },
3856     set_button: function() {
3857         var self = this;
3858         if (this.$button) {
3859             this.$button.remove();
3860         }
3861         this.string = '';
3862         this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3863         this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3864         this.$button.addClass('oe_link').css({'padding':'4px'});
3865         this.$el.append(this.$button);
3866         this.$button.on('click', self.on_click);
3867     },
3868     on_click: function(ev) {
3869         var self = this;
3870         this.popup =  new instance.web.form.FormOpenPopup(this);
3871         this.popup.show_element(
3872             this.field.relation,
3873             this.get('value'),
3874             this.build_context(),
3875             {title: this.string}
3876         );
3877         this.popup.on('create_completed', self, function(r) {
3878             self.set_value(r);
3879         });
3880     },
3881     set_value: function(value_) {
3882         var self = this;
3883         if (value_ instanceof Array) {
3884             value_ = value_[0];
3885         }
3886         value_ = value_ || false;
3887         this.set('value', value_);
3888         this.set_button();
3889      },
3890 });
3891
3892 /**
3893  * Abstract-ish ListView.List subclass adding an "Add an item" row to replace
3894  * the big ugly button in the header.
3895  *
3896  * Requires the implementation of a ``is_readonly`` method (usually a proxy to
3897  * the corresponding field's readonly or effective_readonly property) to
3898  * decide whether the special row should or should not be inserted.
3899  *
3900  * Optionally an ``_add_row_class`` attribute can be set for the class(es) to
3901  * set on the insertion row.
3902  */
3903 instance.web.form.AddAnItemList = instance.web.ListView.List.extend({
3904     pad_table_to: function (count) {
3905         if (!this.view.is_action_enabled('create') || this.is_readonly()) {
3906             this._super(count);
3907             return;
3908         }
3909
3910         this._super(count > 0 ? count - 1 : 0);
3911
3912         var self = this;
3913         var columns = _(this.columns).filter(function (column) {
3914             return column.invisible !== '1';
3915         }).length;
3916         if (this.options.selectable) { columns++; }
3917         if (this.options.deletable) { columns++; }
3918
3919         var $cell = $('<td>', {
3920             colspan: columns,
3921             'class': this._add_row_class || ''
3922         }).append(
3923             $('<a>', {href: '#'}).text(_t("Add an item"))
3924                 .mousedown(function () {
3925                     // FIXME: needs to be an official API somehow
3926                     if (self.view.editor.is_editing()) {
3927                         self.view.__ignore_blur = true;
3928                     }
3929                 })
3930                 .click(function (e) {
3931                     e.preventDefault();
3932                     e.stopPropagation();
3933                     // FIXME: there should also be an API for that one
3934                     if (self.view.editor.form.__blur_timeout) {
3935                         clearTimeout(self.view.editor.form.__blur_timeout);
3936                         self.view.editor.form.__blur_timeout = false;
3937                     }
3938                     self.view.ensure_saved().done(function () {
3939                         self.view.do_add_record();
3940                     });
3941                 }));
3942
3943         var $padding = this.$current.find('tr:not([data-id]):first');
3944         var $newrow = $('<tr>').append($cell);
3945         if ($padding.length) {
3946             $padding.before($newrow);
3947         } else {
3948             this.$current.append($newrow)
3949         }
3950     }
3951 });
3952
3953 /*
3954 # Values: (0, 0,  { fields })    create
3955 #         (1, ID, { fields })    update
3956 #         (2, ID)                remove (delete)
3957 #         (3, ID)                unlink one (target id or target of relation)
3958 #         (4, ID)                link
3959 #         (5)                    unlink all (only valid for one2many)
3960 */
3961 var commands = {
3962     // (0, _, {values})
3963     CREATE: 0,
3964     'create': function (values) {
3965         return [commands.CREATE, false, values];
3966     },
3967     // (1, id, {values})
3968     UPDATE: 1,
3969     'update': function (id, values) {
3970         return [commands.UPDATE, id, values];
3971     },
3972     // (2, id[, _])
3973     DELETE: 2,
3974     'delete': function (id) {
3975         return [commands.DELETE, id, false];
3976     },
3977     // (3, id[, _]) removes relation, but not linked record itself
3978     FORGET: 3,
3979     'forget': function (id) {
3980         return [commands.FORGET, id, false];
3981     },
3982     // (4, id[, _])
3983     LINK_TO: 4,
3984     'link_to': function (id) {
3985         return [commands.LINK_TO, id, false];
3986     },
3987     // (5[, _[, _]])
3988     DELETE_ALL: 5,
3989     'delete_all': function () {
3990         return [5, false, false];
3991     },
3992     // (6, _, ids) replaces all linked records with provided ids
3993     REPLACE_WITH: 6,
3994     'replace_with': function (ids) {
3995         return [6, false, ids];
3996     }
3997 };
3998 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3999     multi_selection: false,
4000     disable_utility_classes: true,
4001     init: function(field_manager, node) {
4002         this._super(field_manager, node);
4003         this.is_loaded = $.Deferred();
4004         this.initial_is_loaded = this.is_loaded;
4005         this.form_last_update = $.Deferred();
4006         this.init_form_last_update = this.form_last_update;
4007         this.is_started = false;
4008         this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
4009         this.dataset.o2m = this;
4010         this.dataset.parent_view = this.view;
4011         this.dataset.child_name = this.name;
4012         var self = this;
4013         this.dataset.on('dataset_changed', this, function() {
4014             self.trigger_on_change();
4015         });
4016         this.set_value([]);
4017     },
4018     start: function() {
4019         this._super.apply(this, arguments);
4020         this.$el.addClass('oe_form_field oe_form_field_one2many');
4021
4022         var self = this;
4023
4024         self.load_views();
4025         this.is_loaded.done(function() {
4026             self.on("change:effective_readonly", self, function() {
4027                 self.is_loaded = self.is_loaded.then(function() {
4028                     self.viewmanager.destroy();
4029                     return $.when(self.load_views()).done(function() {
4030                         self.reload_current_view();
4031                     });
4032                 });
4033             });
4034         });
4035         this.is_started = true;
4036         this.reload_current_view();
4037     },
4038     trigger_on_change: function() {
4039         this.trigger('changed_value');
4040     },
4041     load_views: function() {
4042         var self = this;
4043
4044         var modes = this.node.attrs.mode;
4045         modes = !!modes ? modes.split(",") : ["tree"];
4046         var views = [];
4047         _.each(modes, function(mode) {
4048             if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
4049                 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
4050             }
4051             var view = {
4052                 view_id: false,
4053                 view_type: mode == "tree" ? "list" : mode,
4054                 options: {}
4055             };
4056             if (self.field.views && self.field.views[mode]) {
4057                 view.embedded_view = self.field.views[mode];
4058             }
4059             if(view.view_type === "list") {
4060                 _.extend(view.options, {
4061                     addable: null,
4062                     selectable: self.multi_selection,
4063                     sortable: true,
4064                     import_enabled: false,
4065                     deletable: true
4066                 });
4067                 if (self.get("effective_readonly")) {
4068                     _.extend(view.options, {
4069                         deletable: null,
4070                         reorderable: false,
4071                     });
4072                 }
4073             } else if (view.view_type === "form") {
4074                 if (self.get("effective_readonly")) {
4075                     view.view_type = 'form';
4076                 }
4077                 _.extend(view.options, {
4078                     not_interactible_on_create: true,
4079                 });
4080             } else if (view.view_type === "kanban") {
4081                 _.extend(view.options, {
4082                     confirm_on_delete: false,
4083                 });
4084                 if (self.get("effective_readonly")) {
4085                     _.extend(view.options, {
4086                         action_buttons: false,
4087                         quick_creatable: false,
4088                         creatable: false,
4089                         read_only_mode: true,
4090                     });
4091                 }
4092             }
4093             views.push(view);
4094         });
4095         this.views = views;
4096
4097         this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
4098         this.viewmanager.o2m = self;
4099         var once = $.Deferred().done(function() {
4100             self.init_form_last_update.resolve();
4101         });
4102         var def = $.Deferred().done(function() {
4103             self.initial_is_loaded.resolve();
4104         });
4105         this.viewmanager.on("controller_inited", self, function(view_type, controller) {
4106             controller.o2m = self;
4107             if (view_type == "list") {
4108                 if (self.get("effective_readonly")) {
4109                     controller.on('edit:before', self, function (e) {
4110                         e.cancel = true;
4111                     });
4112                     _(controller.columns).find(function (column) {
4113                         if (!(column instanceof instance.web.list.Handle)) {
4114                             return false;
4115                         }
4116                         column.modifiers.invisible = true;
4117                         return true;
4118                     });
4119                 }
4120             } else if (view_type === "form") {
4121                 if (self.get("effective_readonly")) {
4122                     $(".oe_form_buttons", controller.$el).children().remove();
4123                 }
4124                 controller.on("load_record", self, function(){
4125                      once.resolve();
4126                  });
4127                 controller.on('pager_action_executed',self,self.save_any_view);
4128             } else if (view_type == "graph") {
4129                 self.reload_current_view();
4130             }
4131             def.resolve();
4132         });
4133         this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
4134             $.when(self.save_any_view()).done(function() {
4135                 if (n_mode === "list") {
4136                     $.async_when().done(function() {
4137                         self.reload_current_view();
4138                     });
4139                 }
4140             });
4141         });
4142         $.async_when().done(function () {
4143             self.viewmanager.appendTo(self.$el);
4144         });
4145         return def;
4146     },
4147     reload_current_view: function() {
4148         var self = this;
4149         self.is_loaded = self.is_loaded.then(function() {
4150             var view = self.get_active_view();
4151             if (view.type === "list") {
4152                 return view.controller.reload_content();
4153             } else if (view.type === "form") {
4154                 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
4155                     self.dataset.index = 0;
4156                 }
4157                 var act = function() {
4158                     return view.controller.do_show();
4159                 };
4160                 self.form_last_update = self.form_last_update.then(act, act);
4161                 return self.form_last_update;
4162             } else if (view.controller.do_search) {
4163                 return view.controller.do_search(self.build_domain(), self.dataset.get_context(), []);
4164             }
4165         }, undefined);
4166         return self.is_loaded;
4167     },
4168     get_active_view: function () {
4169         /**
4170          * Returns the current active view if any.
4171          */
4172         return (this.viewmanager && this.viewmanager.active_view);
4173     },
4174     set_value: function(value_) {
4175         value_ = value_ || [];
4176         var self = this;
4177         var view = this.get_active_view();
4178         this.dataset.reset_ids([]);
4179         var ids;
4180         if(value_.length >= 1 && value_[0] instanceof Array) {
4181             ids = [];
4182             _.each(value_, function(command) {
4183                 var obj = {values: command[2]};
4184                 switch (command[0]) {
4185                     case commands.CREATE:
4186                         obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4187                         obj.defaults = {};
4188                         self.dataset.to_create.push(obj);
4189                         self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4190                         ids.push(obj.id);
4191                         return;
4192                     case commands.UPDATE:
4193                         obj['id'] = command[1];
4194                         self.dataset.to_write.push(obj);
4195                         self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
4196                         ids.push(obj.id);
4197                         return;
4198                     case commands.DELETE:
4199                         self.dataset.to_delete.push({id: command[1]});
4200                         return;
4201                     case commands.LINK_TO:
4202                         ids.push(command[1]);
4203                         return;
4204                     case commands.DELETE_ALL:
4205                         self.dataset.delete_all = true;
4206                         return;
4207                 }
4208             });
4209             this._super(ids);
4210             this.dataset.set_ids(ids);
4211         } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
4212             ids = [];
4213             this.dataset.delete_all = true;
4214             _.each(value_, function(command) {
4215                 var obj = {values: command};
4216                 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
4217                 obj.defaults = {};
4218                 self.dataset.to_create.push(obj);
4219                 self.dataset.cache.push(_.clone(obj));
4220                 ids.push(obj.id);
4221             });
4222             this._super(ids);
4223             this.dataset.set_ids(ids);
4224         } else {
4225             this._super(value_);
4226             this.dataset.reset_ids(value_);
4227         }
4228         if (this.dataset.index === null && this.dataset.ids.length > 0) {
4229             this.dataset.index = 0;
4230         }
4231         this.trigger_on_change();
4232         if (this.is_started) {
4233             return self.reload_current_view();
4234         } else {
4235             return $.when();
4236         }
4237     },
4238     get_value: function() {
4239         var self = this;
4240         if (!this.dataset)
4241             return [];
4242         var val = this.dataset.delete_all ? [commands.delete_all()] : [];
4243         val = val.concat(_.map(this.dataset.ids, function(id) {
4244             var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
4245             if (alter_order) {
4246                 return commands.create(alter_order.values);
4247             }
4248             alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
4249             if (alter_order) {
4250                 return commands.update(alter_order.id, alter_order.values);
4251             }
4252             return commands.link_to(id);
4253         }));
4254         return val.concat(_.map(
4255             this.dataset.to_delete, function(x) {
4256                 return commands['delete'](x.id);}));
4257     },
4258     commit_value: function() {
4259         return this.save_any_view();
4260     },
4261     save_any_view: function() {
4262         var view = this.get_active_view();
4263         if (view) {
4264             if (this.viewmanager.active_view.type === "form") {
4265                 if (view.controller.is_initialized.state() !== 'resolved') {
4266                     return $.when(false);
4267                 }
4268                 return $.when(view.controller.save());
4269             } else if (this.viewmanager.active_view.type === "list") {
4270                 return $.when(view.controller.ensure_saved());
4271             }
4272         }
4273         return $.when(false);
4274     },
4275     is_syntax_valid: function() {
4276         var view = this.get_active_view();
4277         if (!view){
4278             return true;
4279         }
4280         switch (this.viewmanager.active_view.type) {
4281         case 'form':
4282             return _(view.controller.fields).chain()
4283                 .invoke('is_valid')
4284                 .all(_.identity)
4285                 .value();
4286         case 'list':
4287             return view.controller.is_valid();
4288         }
4289         return true;
4290     },
4291 });
4292
4293 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
4294     template: 'One2Many.viewmanager',
4295     init: function(parent, dataset, views, flags) {
4296         this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
4297         this.registry = instance.web.views.extend({
4298             list: 'instance.web.form.One2ManyListView',
4299             form: 'instance.web.form.One2ManyFormView',
4300         });
4301         this.__ignore_blur = false;
4302     },
4303     switch_mode: function(mode, unused) {
4304         if (mode !== 'form') {
4305             return this._super(mode, unused);
4306         }
4307         var self = this;
4308         var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
4309         var pop = new instance.web.form.FormOpenPopup(this);
4310         pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4311             title: _t("Open: ") + self.o2m.string,
4312             create_function: function(data, options) {
4313                 return self.o2m.dataset.create(data, options).done(function(r) {
4314                     self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4315                     self.o2m.dataset.trigger("dataset_changed", r);
4316                 });
4317             },
4318             write_function: function(id, data, options) {
4319                 return self.o2m.dataset.write(id, data, {}).done(function() {
4320                     self.o2m.reload_current_view();
4321                 });
4322             },
4323             alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4324             parent_view: self.o2m.view,
4325             child_name: self.o2m.name,
4326             read_function: function() {
4327                 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4328             },
4329             form_view_options: {'not_interactible_on_create':true},
4330             readonly: self.o2m.get("effective_readonly")
4331         });
4332         pop.on("elements_selected", self, function() {
4333             self.o2m.reload_current_view();
4334         });
4335     },
4336 });
4337
4338 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
4339     get_context: function() {
4340         this.context = this.o2m.build_context();
4341         return this.context;
4342     }
4343 });
4344
4345 instance.web.form.One2ManyListView = instance.web.ListView.extend({
4346     _template: 'One2Many.listview',
4347     init: function (parent, dataset, view_id, options) {
4348         this._super(parent, dataset, view_id, _.extend(options || {}, {
4349             GroupsType: instance.web.form.One2ManyGroups,
4350             ListType: instance.web.form.One2ManyList
4351         }));
4352         this.on('edit:after', this, this.proxy('_after_edit'));
4353         this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
4354
4355         this.records
4356             .bind('add', this.proxy("changed_records"))
4357             .bind('edit', this.proxy("changed_records"))
4358             .bind('remove', this.proxy("changed_records"));
4359     },
4360     start: function () {
4361         var ret = this._super();
4362         this.$el
4363             .off('mousedown.handleButtons')
4364             .on('mousedown.handleButtons', 'table button, div a.oe_m2o_cm_button', this.proxy('_button_down'));
4365         return ret;
4366     },
4367     changed_records: function () {
4368         this.o2m.trigger_on_change();
4369     },
4370     is_valid: function () {
4371         var self = this;
4372         if (!this.fields_view || !this.editable()){
4373             return true;
4374         }
4375         var r;
4376         return _.every(this.records.records, function(record){
4377             r = record;
4378             _.each(self.editor.form.fields, function(field){
4379                 field._inhibit_on_change_flag = true;
4380                 field.set_value(r.attributes[field.name]);
4381                 field._inhibit_on_change_flag = false;
4382             });
4383             return _.every(self.editor.form.fields, function(field){
4384                 field.process_modifiers();
4385                 field._check_css_flags();
4386                 return field.is_valid();
4387             });
4388         });
4389     },
4390     do_add_record: function () {
4391         if (this.editable()) {
4392             this._super.apply(this, arguments);
4393         } else {
4394             var self = this;
4395             var pop = new instance.web.form.SelectCreatePopup(this);
4396             pop.select_element(
4397                 self.o2m.field.relation,
4398                 {
4399                     title: _t("Create: ") + self.o2m.string,
4400                     initial_view: "form",
4401                     alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4402                     create_function: function(data, options) {
4403                         return self.o2m.dataset.create(data, options).done(function(r) {
4404                             self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
4405                             self.o2m.dataset.trigger("dataset_changed", r);
4406                         });
4407                     },
4408                     read_function: function() {
4409                         return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4410                     },
4411                     parent_view: self.o2m.view,
4412                     child_name: self.o2m.name,
4413                     form_view_options: {'not_interactible_on_create':true}
4414                 },
4415                 self.o2m.build_domain(),
4416                 self.o2m.build_context()
4417             );
4418             pop.on("elements_selected", self, function() {
4419                 self.o2m.reload_current_view();
4420             });
4421         }
4422     },
4423     do_activate_record: function(index, id) {
4424         var self = this;
4425         var pop = new instance.web.form.FormOpenPopup(self);
4426         pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4427             title: _t("Open: ") + self.o2m.string,
4428             write_function: function(id, data) {
4429                 return self.o2m.dataset.write(id, data, {}).done(function() {
4430                     self.o2m.reload_current_view();
4431                 });
4432             },
4433             alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4434             parent_view: self.o2m.view,
4435             child_name: self.o2m.name,
4436             read_function: function() {
4437                 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4438             },
4439             form_view_options: {'not_interactible_on_create':true},
4440             readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4441         });
4442     },
4443     do_button_action: function (name, id, callback) {
4444         if (!_.isNumber(id)) {
4445             instance.webclient.notification.warn(
4446                 _t("Action Button"),
4447                 _t("The o2m record must be saved before an action can be used"));
4448             return;
4449         }
4450         var parent_form = this.o2m.view;
4451         var self = this;
4452         this.ensure_saved().then(function () {
4453             if (parent_form)
4454                 return parent_form.save();
4455             else
4456                 return $.when();
4457         }).done(function () {
4458             var ds = self.o2m.dataset;
4459             var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) {
4460                 return value.length;
4461             });
4462             if (!self.o2m.options.reload_on_button && !cached_records) {
4463                 self.handle_button(name, id, callback);
4464             }else {
4465                 self.handle_button(name, id, function(){
4466                     self.o2m.view.reload();
4467                 });
4468             }
4469         });
4470     },
4471
4472     _after_edit: function () {
4473         this.__ignore_blur = false;
4474         this.editor.form.on('blurred', this, this._on_form_blur);
4475
4476         // The form's blur thing may be jiggered during the edition setup,
4477         // potentially leading to the o2m instasaving the row. Cancel any
4478         // blurring triggered the edition startup here
4479         this.editor.form.widgetFocused();
4480     },
4481     _before_unedit: function () {
4482         this.editor.form.off('blurred', this, this._on_form_blur);
4483     },
4484     _button_down: function () {
4485         // If a button is clicked (usually some sort of action button), it's
4486         // the button's responsibility to ensure the editable list is in the
4487         // correct state -> ignore form blurring
4488         this.__ignore_blur = true;
4489     },
4490     /**
4491      * Handles blurring of the nested form (saves the currently edited row),
4492      * unless the flag to ignore the event is set to ``true``
4493      *
4494      * Makes the internal form go away
4495      */
4496     _on_form_blur: function () {
4497         if (this.__ignore_blur) {
4498             this.__ignore_blur = false;
4499             return;
4500         }
4501         // FIXME: why isn't there an API for this?
4502         if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4503             this.ensure_saved();
4504             return;
4505         }
4506         this.cancel_edition();
4507     },
4508     keypress_ENTER: function () {
4509         // blurring caused by hitting the [Return] key, should skip the
4510         // autosave-on-blur and let the handler for [Return] do its thing (save
4511         // the current row *anyway*, then create a new one/edit the next one)
4512         this.__ignore_blur = true;
4513         this._super.apply(this, arguments);
4514     },
4515     do_delete: function (ids) {
4516         var confirm = window.confirm;
4517         window.confirm = function () { return true; };
4518         try {
4519             return this._super(ids);
4520         } finally {
4521             window.confirm = confirm;
4522         }
4523     },
4524     reload_record: function (record, options) {
4525         if (!options || !options['do_not_evict']) {
4526             // Evict record.id from cache to ensure it will be reloaded correctly
4527             this.dataset.evict_record(record.get('id'));
4528         }
4529
4530         return this._super(record);
4531     }
4532 });
4533 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4534     setup_resequence_rows: function () {
4535         if (!this.view.o2m.get('effective_readonly')) {
4536             this._super.apply(this, arguments);
4537         }
4538     }
4539 });
4540 instance.web.form.One2ManyList = instance.web.form.AddAnItemList.extend({
4541     _add_row_class: 'oe_form_field_one2many_list_row_add',
4542     is_readonly: function () {
4543         return this.view.o2m.get('effective_readonly');
4544     },
4545 });
4546
4547 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4548     form_template: 'One2Many.formview',
4549     load_form: function(data) {
4550         this._super(data);
4551         var self = this;
4552         this.$buttons.find('button.oe_form_button_create').click(function() {
4553             self.save().done(self.on_button_new);
4554         });
4555     },
4556     do_notify_change: function() {
4557         if (this.dataset.parent_view) {
4558             this.dataset.parent_view.do_notify_change();
4559         } else {
4560             this._super.apply(this, arguments);
4561         }
4562     }
4563 });
4564
4565 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4566     template: "FieldMany2ManyTags",
4567     tag_template: "FieldMany2ManyTag",
4568     init: function() {
4569         this._super.apply(this, arguments);
4570         instance.web.form.CompletionFieldMixin.init.call(this);
4571         this.set({"value": []});
4572         this._display_orderer = new instance.web.DropMisordered();
4573         this._drop_shown = false;
4574     },
4575     initialize_texttext: function(){
4576         var self = this;
4577         return {
4578             plugins : 'tags arrow autocomplete',
4579             autocomplete: {
4580                 render: function(suggestion) {
4581                     return $('<span class="text-label"/>').
4582                              data('index', suggestion['index']).html(suggestion['label']);
4583                 }
4584             },
4585             ext: {
4586                 autocomplete: {
4587                     selectFromDropdown: function() {
4588                         this.trigger('hideDropdown');
4589                         var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4590                         var data = self.search_result[index];
4591                         if (data.id) {
4592                             self.add_id(data.id);
4593                         } else {
4594                             self.ignore_blur = true;
4595                             data.action();
4596                         }
4597                         this.trigger('setSuggestions', {result : []});
4598                     },
4599                 },
4600                 tags: {
4601                     isTagAllowed: function(tag) {
4602                         return !!tag.name;
4603
4604                     },
4605                     removeTag: function(tag) {
4606                         var id = tag.data("id");
4607                         self.set({"value": _.without(self.get("value"), id)});
4608                     },
4609                     renderTag: function(stuff) {
4610                         return $.fn.textext.TextExtTags.prototype.renderTag.
4611                             call(this, stuff).data("id", stuff.id);
4612                     },
4613                 },
4614                 itemManager: {
4615                     itemToString: function(item) {
4616                         return item.name;
4617                     },
4618                 },
4619                 core: {
4620                     onSetInputData: function(e, data) {
4621                         if (data === '') {
4622                             this._plugins.autocomplete._suggestions = null;
4623                         }
4624                         this.input().val(data);
4625                     },
4626                 },
4627             },
4628         }
4629     },
4630     initialize_content: function() {
4631         if (this.get("effective_readonly"))
4632             return;
4633         var self = this;
4634         self.ignore_blur = false;
4635         self.$text = this.$("textarea");
4636         self.$text.textext(self.initialize_texttext()).bind('getSuggestions', function(e, data) {
4637             var _this = this;
4638             var str = !!data ? data.query || '' : '';
4639             self.get_search_result(str).done(function(result) {
4640                 self.search_result = result;
4641                 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4642                     return _.extend(el, {index:i});
4643                 })});
4644             });
4645         }).bind('hideDropdown', function() {
4646             self._drop_shown = false;
4647         }).bind('showDropdown', function() {
4648             self._drop_shown = true;
4649         });
4650         self.tags = self.$text.textext()[0].tags();
4651         self.$text
4652             .focusin(function () {
4653                 self.trigger('focused');
4654                 self.ignore_blur = false;
4655             })
4656             .focusout(function() {
4657                 self.$text.trigger("setInputData", "");
4658                 if (!self.ignore_blur) {
4659                     self.trigger('blurred');
4660                 }
4661             }).keydown(function(e) {
4662                 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4663                     self.$text.textext()[0].autocomplete().selectFromDropdown();
4664                 }
4665             });
4666     },
4667     // WARNING: duplicated in 4 other M2M widgets
4668     set_value: function(value_) {
4669         value_ = value_ || [];
4670         if (value_.length >= 1 && value_[0] instanceof Array) {
4671             // value_ is a list of m2m commands. We only process
4672             // LINK_TO and REPLACE_WITH in this context
4673             var val = [];
4674             _.each(value_, function (command) {
4675                 if (command[0] === commands.LINK_TO) {
4676                     val.push(command[1]);                   // (4, id[, _])
4677                 } else if (command[0] === commands.REPLACE_WITH) {
4678                     val = command[2];                       // (6, _, ids)
4679                 }
4680             });
4681             value_ = val;
4682         }
4683         this._super(value_);
4684     },
4685     is_false: function() {
4686         return _(this.get("value")).isEmpty();
4687     },
4688     get_value: function() {
4689         var tmp = [commands.replace_with(this.get("value"))];
4690         return tmp;
4691     },
4692     get_search_blacklist: function() {
4693         return this.get("value");
4694     },
4695     map_tag: function(data){
4696         return _.map(data, function(el) {return {name: el[1], id:el[0]};})
4697     },
4698     get_render_data: function(ids){
4699         var self = this;
4700         var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4701         return dataset.name_get(ids);
4702     },
4703     render_tag: function(data) {
4704         var self = this;
4705         if (! self.get("effective_readonly")) {
4706             self.tags.containerElement().children().remove();
4707             self.$('textarea').css("padding-left", "3px");
4708             self.tags.addTags(self.map_tag(data));
4709         } else {
4710             self.$el.html(QWeb.render(self.tag_template, {elements: data}));
4711         }
4712     },
4713     render_value: function() {
4714         var self = this;
4715         var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4716         var values = self.get("value");
4717         var handle_names = function(data) {
4718             if (self.isDestroyed())
4719                 return;
4720             var indexed = {};
4721             _.each(data, function(el) {
4722                 indexed[el[0]] = el;
4723             });
4724             data = _.map(values, function(el) { return indexed[el]; });
4725             self.render_tag(data);
4726         }
4727         if (! values || values.length > 0) {
4728             return this._display_orderer.add(self.get_render_data(values)).done(handle_names);
4729         } else {
4730             handle_names([]);
4731         }
4732     },
4733     add_id: function(id) {
4734         this.set({'value': _.uniq(this.get('value').concat([id]))});
4735     },
4736     focus: function () {
4737         var input = this.$text && this.$text[0];
4738         return input ? input.focus() : false;
4739     },
4740     set_dimensions: function (height, width) {
4741         this._super(height, width);        
4742         this.$("textarea").css({
4743             width: width,
4744             minHeight: height
4745         });
4746     },    
4747     _search_create_popup: function() {
4748         self.ignore_blur = true;
4749         return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
4750     },
4751 });
4752
4753 /**
4754     widget options:
4755     - reload_on_button: Reload the whole form view if click on a button in a list view.
4756         If you see this options, do not use it, it's basically a dirty hack to make one
4757         precise o2m to behave the way we want.
4758 */
4759 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4760     multi_selection: false,
4761     disable_utility_classes: true,
4762     init: function(field_manager, node) {
4763         this._super(field_manager, node);
4764         this.is_loaded = $.Deferred();
4765         this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4766         this.dataset.m2m = this;
4767         var self = this;
4768         this.dataset.on('unlink', self, function(ids) {
4769             self.dataset_changed();
4770         });
4771         this.set_value([]);
4772         this.list_dm = new instance.web.DropMisordered();
4773         this.render_value_dm = new instance.web.DropMisordered();
4774     },
4775     initialize_content: function() {
4776         var self = this;
4777
4778         this.$el.addClass('oe_form_field oe_form_field_many2many');
4779
4780         this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4781                     'addable': null,
4782                     'deletable': this.get("effective_readonly") ? false : true,
4783                     'selectable': this.multi_selection,
4784                     'sortable': false,
4785                     'reorderable': false,
4786                     'import_enabled': false,
4787             });
4788         var embedded = (this.field.views || {}).tree;
4789         if (embedded) {
4790             this.list_view.set_embedded_view(embedded);
4791         }
4792         this.list_view.m2m_field = this;
4793         var loaded = $.Deferred();
4794         this.list_view.on("list_view_loaded", this, function() {
4795             loaded.resolve();
4796         });
4797         this.list_view.appendTo(this.$el);
4798
4799         var old_def = self.is_loaded;
4800         self.is_loaded = $.Deferred().done(function() {
4801             old_def.resolve();
4802         });
4803         this.list_dm.add(loaded).then(function() {
4804             self.is_loaded.resolve();
4805         });
4806     },
4807     destroy_content: function() {
4808         this.list_view.destroy();
4809         this.list_view = undefined;
4810     },
4811     // WARNING: duplicated in 4 other M2M widgets
4812     set_value: function(value_) {
4813         value_ = value_ || [];
4814         if (value_.length >= 1 && value_[0] instanceof Array) {
4815             // value_ is a list of m2m commands. We only process
4816             // LINK_TO and REPLACE_WITH in this context
4817             var val = [];
4818             _.each(value_, function (command) {
4819                 if (command[0] === commands.LINK_TO) {
4820                     val.push(command[1]);                   // (4, id[, _])
4821                 } else if (command[0] === commands.REPLACE_WITH) {
4822                     val = command[2];                       // (6, _, ids)
4823                 }
4824             });
4825             value_ = val;
4826         }
4827         this._super(value_);
4828     },
4829     get_value: function() {
4830         return [commands.replace_with(this.get('value'))];
4831     },
4832     is_false: function () {
4833         return _(this.get("value")).isEmpty();
4834     },
4835     render_value: function() {
4836         var self = this;
4837         this.dataset.set_ids(this.get("value"));
4838         this.render_value_dm.add(this.is_loaded).then(function() {
4839             return self.list_view.reload_content();
4840         });
4841     },
4842     dataset_changed: function() {
4843         this.internal_set_value(this.dataset.ids);
4844     },
4845 });
4846
4847 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4848     get_context: function() {
4849         this.context = this.m2m.build_context();
4850         return this.context;
4851     }
4852 });
4853
4854 /**
4855  * @class
4856  * @extends instance.web.ListView
4857  */
4858 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4859     init: function (parent, dataset, view_id, options) {
4860         this._super(parent, dataset, view_id, _.extend(options || {}, {
4861             ListType: instance.web.form.Many2ManyList,
4862         }));
4863     },
4864     do_add_record: function () {
4865         var pop = new instance.web.form.SelectCreatePopup(this);
4866         pop.select_element(
4867             this.model,
4868             {
4869                 title: _t("Add: ") + this.m2m_field.string,
4870                 no_create: this.m2m_field.options.no_create,
4871             },
4872             new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4873             this.m2m_field.build_context()
4874         );
4875         var self = this;
4876         pop.on("elements_selected", self, function(element_ids) {
4877             var reload = false;
4878             _(element_ids).each(function (id) {
4879                 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4880                     self.dataset.set_ids(self.dataset.ids.concat([id]));
4881                     self.m2m_field.dataset_changed();
4882                     reload = true;
4883                 }
4884             });
4885             if (reload) {
4886                 self.reload_content();
4887             }
4888         });
4889     },
4890     do_activate_record: function(index, id) {
4891         var self = this;
4892         var pop = new instance.web.form.FormOpenPopup(this);
4893         pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4894             title: _t("Open: ") + this.m2m_field.string,
4895             readonly: this.getParent().get("effective_readonly")
4896         });
4897         pop.on('write_completed', self, self.reload_content);
4898     },
4899     do_button_action: function(name, id, callback) {
4900         var self = this;
4901         var _sup = _.bind(this._super, this);
4902         if (! this.m2m_field.options.reload_on_button) {
4903             return _sup(name, id, callback);
4904         } else {
4905             return this.m2m_field.view.save().then(function() {
4906                 return _sup(name, id, function() {
4907                     self.m2m_field.view.reload();
4908                 });
4909             });
4910         }
4911      },
4912     is_action_enabled: function () { return true; },
4913 });
4914 instance.web.form.Many2ManyList = instance.web.form.AddAnItemList.extend({
4915     _add_row_class: 'oe_form_field_many2many_list_row_add',
4916     is_readonly: function () {
4917         return this.view.m2m_field.get('effective_readonly');
4918     }
4919 });
4920
4921 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4922     disable_utility_classes: true,
4923     init: function(field_manager, node) {
4924         this._super(field_manager, node);
4925         instance.web.form.CompletionFieldMixin.init.call(this);
4926         m2m_kanban_lazy_init();
4927         this.is_loaded = $.Deferred();
4928         this.initial_is_loaded = this.is_loaded;
4929
4930         var self = this;
4931         this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4932         this.dataset.m2m = this;
4933         this.dataset.on('unlink', self, function(ids) {
4934             self.dataset_changed();
4935         });
4936     },
4937     start: function() {
4938         this._super.apply(this, arguments);
4939
4940         var self = this;
4941
4942         self.load_view();
4943         self.on("change:effective_readonly", self, function() {
4944             self.is_loaded = self.is_loaded.then(function() {
4945                 self.kanban_view.destroy();
4946                 return $.when(self.load_view()).done(function() {
4947                     self.render_value();
4948                 });
4949             });
4950         });
4951     },
4952     // WARNING: duplicated in 4 other M2M widgets
4953     set_value: function(value_) {
4954         value_ = value_ || [];
4955         if (value_.length >= 1 && value_[0] instanceof Array) {
4956             // value_ is a list of m2m commands. We only process
4957             // LINK_TO and REPLACE_WITH in this context
4958             var val = [];
4959             _.each(value_, function (command) {
4960                 if (command[0] === commands.LINK_TO) {
4961                     val.push(command[1]);                   // (4, id[, _])
4962                 } else if (command[0] === commands.REPLACE_WITH) {
4963                     val = command[2];                       // (6, _, ids)
4964                 }
4965             });
4966             value_ = val;
4967         }
4968         this._super(value_);
4969     },
4970     get_value: function() {
4971         return [commands.replace_with(this.get('value'))];
4972     },
4973     load_view: function() {
4974         var self = this;
4975         this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4976                     'create_text': _t("Add"),
4977                     'creatable': self.get("effective_readonly") ? false : true,
4978                     'quick_creatable': self.get("effective_readonly") ? false : true,
4979                     'read_only_mode': self.get("effective_readonly") ? true : false,
4980                     'confirm_on_delete': false,
4981             });
4982         var embedded = (this.field.views || {}).kanban;
4983         if (embedded) {
4984             this.kanban_view.set_embedded_view(embedded);
4985         }
4986         this.kanban_view.m2m = this;
4987         var loaded = $.Deferred();
4988         this.kanban_view.on("kanban_view_loaded",self,function() {
4989             self.initial_is_loaded.resolve();
4990             loaded.resolve();
4991         });
4992         this.kanban_view.on('switch_mode', this, this.open_popup);
4993         $.async_when().done(function () {
4994             self.kanban_view.appendTo(self.$el);
4995         });
4996         return loaded;
4997     },
4998     render_value: function() {
4999         var self = this;
5000         this.dataset.set_ids(this.get("value"));
5001         this.is_loaded = this.is_loaded.then(function() {
5002             return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
5003         });
5004     },
5005     dataset_changed: function() {
5006         this.set({'value': this.dataset.ids});
5007     },
5008     open_popup: function(type, unused) {
5009         if (type !== "form")
5010             return;
5011         var self = this;
5012         var pop;
5013         if (this.dataset.index === null) {
5014             pop = new instance.web.form.SelectCreatePopup(this);
5015             pop.select_element(
5016                 this.field.relation,
5017                 {
5018                     title: _t("Add: ") + this.string
5019                 },
5020                 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
5021                 this.build_context()
5022             );
5023             pop.on("elements_selected", self, function(element_ids) {
5024                 _.each(element_ids, function(one_id) {
5025                     if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
5026                         self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
5027                         self.dataset_changed();
5028                         self.render_value();
5029                     }
5030                 });
5031             });
5032         } else {
5033             var id = self.dataset.ids[self.dataset.index];
5034             pop = new instance.web.form.FormOpenPopup(this);
5035             pop.show_element(self.field.relation, id, self.build_context(), {
5036                 title: _t("Open: ") + self.string,
5037                 write_function: function(id, data, options) {
5038                     return self.dataset.write(id, data, {}).done(function() {
5039                         self.render_value();
5040                     });
5041                 },
5042                 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
5043                 parent_view: self.view,
5044                 child_name: self.name,
5045                 readonly: self.get("effective_readonly")
5046             });
5047         }
5048     },
5049     add_id: function(id) {
5050         this.quick_create.add_id(id);
5051     },
5052 });
5053
5054 function m2m_kanban_lazy_init() {
5055 if (instance.web.form.Many2ManyKanbanView)
5056     return;
5057 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
5058     quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
5059     _is_quick_create_enabled: function() {
5060         return this._super() && ! this.group_by;
5061     },
5062 });
5063 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
5064     template: 'Many2ManyKanban.quick_create',
5065
5066     /**
5067      * close_btn: If true, the widget will display a "Close" button able to trigger
5068      * a "close" event.
5069      */
5070     init: function(parent, dataset, context, buttons) {
5071         this._super(parent);
5072         this.m2m = this.getParent().view.m2m;
5073         this.m2m.quick_create = this;
5074         this._dataset = dataset;
5075         this._buttons = buttons || false;
5076         this._context = context || {};
5077     },
5078     start: function () {
5079         var self = this;
5080         self.$text = this.$el.find('input').css("width", "200px");
5081         self.$text.textext({
5082             plugins : 'arrow autocomplete',
5083             autocomplete: {
5084                 render: function(suggestion) {
5085                     return $('<span class="text-label"/>').
5086                              data('index', suggestion['index']).html(suggestion['label']);
5087                 }
5088             },
5089             ext: {
5090                 autocomplete: {
5091                     selectFromDropdown: function() {
5092                         $(this).trigger('hideDropdown');
5093                         var index = Number(this.selectedSuggestionElement().children().children().data('index'));
5094                         var data = self.search_result[index];
5095                         if (data.id) {
5096                             self.add_id(data.id);
5097                         } else {
5098                             data.action();
5099                         }
5100                     },
5101                 },
5102                 itemManager: {
5103                     itemToString: function(item) {
5104                         return item.name;
5105                     },
5106                 },
5107             },
5108         }).bind('getSuggestions', function(e, data) {
5109             var _this = this;
5110             var str = !!data ? data.query || '' : '';
5111             self.m2m.get_search_result(str).done(function(result) {
5112                 self.search_result = result;
5113                 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
5114                     return _.extend(el, {index:i});
5115                 })});
5116             });
5117         });
5118         self.$text.focusout(function() {
5119             self.$text.val("");
5120         });
5121     },
5122     focus: function() {
5123         this.$text[0].focus();
5124     },
5125     add_id: function(id) {
5126         var self = this;
5127         self.$text.val("");
5128         self.trigger('added', id);
5129         this.m2m.dataset_changed();
5130     },
5131 });
5132 }
5133
5134 /**
5135  * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
5136  */
5137 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
5138     template: "AbstractFormPopup.render",
5139     /**
5140      *  options:
5141      *  -readonly: only applicable when not in creation mode, default to false
5142      * - alternative_form_view
5143      * - view_id
5144      * - write_function
5145      * - read_function
5146      * - create_function
5147      * - parent_view
5148      * - child_name
5149      * - form_view_options
5150      */
5151     init_popup: function(model, row_id, domain, context, options) {
5152         this.row_id = row_id;
5153         this.model = model;
5154         this.domain = domain || [];
5155         this.context = context || {};
5156         this.options = options;
5157         _.defaults(this.options, {});
5158     },
5159     init_dataset: function() {
5160         var self = this;
5161         this.created_elements = [];
5162         this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
5163         this.dataset.read_function = this.options.read_function;
5164         this.dataset.create_function = function(data, options, sup) {
5165             var fct = self.options.create_function || sup;
5166             return fct.call(this, data, options).done(function(r) {
5167                 self.trigger('create_completed saved', r);
5168                 self.created_elements.push(r);
5169             });
5170         };
5171         this.dataset.write_function = function(id, data, options, sup) {
5172             var fct = self.options.write_function || sup;
5173             return fct.call(this, id, data, options).done(function(r) {
5174                 self.trigger('write_completed saved', r);
5175             });
5176         };
5177         this.dataset.parent_view = this.options.parent_view;
5178         this.dataset.child_name = this.options.child_name;
5179     },
5180     display_popup: function() {
5181         var self = this;
5182         this.renderElement();
5183         var dialog = new instance.web.Dialog(this, {
5184             dialogClass: 'oe_act_window',
5185             title: this.options.title || "",
5186         }, this.$el).open();
5187         dialog.on('closing', this, function (e){
5188             self.check_exit(true);
5189         });
5190         this.$buttonpane = dialog.$buttons;
5191         this.start();
5192     },
5193     setup_form_view: function() {
5194         var self = this;
5195         if (this.row_id) {
5196             this.dataset.ids = [this.row_id];
5197             this.dataset.index = 0;
5198         } else {
5199             this.dataset.index = null;
5200         }
5201         var options = _.clone(self.options.form_view_options) || {};
5202         if (this.row_id !== null) {
5203             options.initial_mode = this.options.readonly ? "view" : "edit";
5204         }
5205         _.extend(options, {
5206             $buttons: this.$buttonpane,
5207         });
5208         this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
5209         if (this.options.alternative_form_view) {
5210             this.view_form.set_embedded_view(this.options.alternative_form_view);
5211         }
5212         this.view_form.appendTo(this.$(".oe_popup_form").show());
5213         this.view_form.on("form_view_loaded", self, function() {
5214             var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
5215             self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
5216                 multi_select: multi_select,
5217                 readonly: self.row_id !== null && self.options.readonly,
5218             }));
5219             var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
5220             $snbutton.click(function() {
5221                 $.when(self.view_form.save()).done(function() {
5222                     self.view_form.reload_mutex.exec(function() {
5223                         self.view_form.on_button_new();
5224                     });
5225                 });
5226             });
5227             var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
5228             $sbutton.click(function() {
5229                 $.when(self.view_form.save()).done(function() {
5230                     self.view_form.reload_mutex.exec(function() {
5231                         self.check_exit();
5232                     });
5233                 });
5234             });
5235             var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
5236             $cbutton.click(function() {
5237                 self.view_form.trigger('on_button_cancel');
5238                 self.check_exit();
5239             });
5240             self.view_form.do_show();
5241         });
5242     },
5243     select_elements: function(element_ids) {
5244         this.trigger("elements_selected", element_ids);
5245     },
5246     check_exit: function(no_destroy) {
5247         if (this.created_elements.length > 0) {
5248             this.select_elements(this.created_elements);
5249             this.created_elements = [];
5250         }
5251         this.trigger('closed');
5252         this.destroy();
5253     },
5254     destroy: function () {
5255         this.trigger('closed');
5256         if (this.$el.is(":data(bs.modal)")) {
5257             this.$el.parents('.modal').modal('hide');
5258         }
5259         this._super();
5260     },
5261 });
5262
5263 /**
5264  * Class to display a popup containing a form view.
5265  */
5266 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
5267     show_element: function(model, row_id, context, options) {
5268         this.init_popup(model, row_id, [], context,  options);
5269         _.defaults(this.options, {
5270         });
5271         this.display_popup();
5272     },
5273     start: function() {
5274         this._super();
5275         this.init_dataset();
5276         this.setup_form_view();
5277     },
5278 });
5279
5280 /**
5281  * Class to display a popup to display a list to search a row. It also allows
5282  * to switch to a form view to create a new row.
5283  */
5284 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
5285     /**
5286      * options:
5287      * - initial_ids
5288      * - initial_view: form or search (default search)
5289      * - disable_multiple_selection
5290      * - list_view_options
5291      */
5292     select_element: function(model, options, domain, context) {
5293         this.init_popup(model, null, domain, context, options);
5294         var self = this;
5295         _.defaults(this.options, {
5296             initial_view: "search",
5297         });
5298         this.initial_ids = this.options.initial_ids;
5299         this.display_popup();
5300     },
5301     start: function() {
5302         this.init_dataset();
5303         if (this.options.initial_view == "search") {
5304             var context = instance.web.pyeval.sync_eval_domains_and_contexts({
5305                 domains: [],
5306                 contexts: [this.context]
5307             }).context;
5308             var search_defaults = {};
5309             _.each(context, function (value_, key) {
5310                 var match = /^search_default_(.*)$/.exec(key);
5311                 if (match) {
5312                     search_defaults[match[1]] = value_;
5313                 }
5314             });
5315             this.setup_search_view(search_defaults);
5316         } else { // "form"
5317             this.new_object();
5318         }
5319     },
5320     setup_search_view: function(search_defaults) {
5321         var self = this;
5322         if (this.searchview) {
5323             this.searchview.destroy();
5324         }
5325         var $buttons = this.$('.o-search-options');
5326         this.searchview = new instance.web.SearchView(this,
5327                 this.dataset, false,  search_defaults, {$buttons: $buttons});
5328         this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
5329             if (self.initial_ids) {
5330                 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
5331                     contexts.concat(self.context), groupbys);
5332                 self.initial_ids = undefined;
5333             } else {
5334                 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
5335             }
5336         });
5337         this.searchview.appendTo(this.$(".o-popup-search")).done(function() {
5338             self.searchview.toggle_visibility(true);
5339             self.view_list = new instance.web.form.SelectCreateListView(self,
5340                     self.dataset, false,
5341                     _.extend({'deletable': false,
5342                         'selectable': !self.options.disable_multiple_selection,
5343                         'import_enabled': false,
5344                         '$buttons': self.$buttonpane,
5345                         'disable_editable_mode': true,
5346                         '$pager': self.$('.oe_popup_list_pager'),
5347                     }, self.options.list_view_options || {}));
5348             self.view_list.on('edit:before', self, function (e) {
5349                 e.cancel = true;
5350             });
5351             self.view_list.popup = self;
5352             self.view_list.appendTo(self.$(".oe_popup_list").show()).then(function() {
5353                 self.view_list.do_show();
5354             }).then(function() {
5355                 self.searchview.do_search();
5356             });
5357             self.view_list.on("list_view_loaded", self, function() {
5358                 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
5359                 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
5360                 $cbutton.click(function() {
5361                     self.destroy();
5362                 });
5363                 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
5364                 $sbutton.click(function() {
5365                     self.select_elements(self.selected_ids);
5366                     self.destroy();
5367                 });
5368                 $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
5369                 $cbutton.click(function() {
5370                     self.new_object();
5371                 });
5372             });
5373         });        
5374     },
5375     do_search: function(domains, contexts, groupbys) {
5376         var self = this;
5377         instance.web.pyeval.eval_domains_and_contexts({
5378             domains: domains || [],
5379             contexts: contexts || [],
5380             group_by_seq: groupbys || []
5381         }).done(function (results) {
5382             self.view_list.do_search(results.domain, results.context, results.group_by);
5383         });
5384     },
5385     on_click_element: function(ids) {
5386         var self = this;
5387         this.selected_ids = ids || [];
5388         if(this.selected_ids.length > 0) {
5389             self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
5390         } else {
5391             self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
5392         }
5393     },
5394     new_object: function() {
5395         if (this.searchview) {
5396             this.searchview.do_hide();
5397         }
5398         if (this.view_list) {
5399             this.view_list.do_hide();
5400         }
5401         this.setup_form_view();
5402     },
5403 });
5404
5405 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
5406     do_add_record: function () {
5407         this.popup.new_object();
5408     },
5409     select_record: function(index) {
5410         this.popup.select_elements([this.dataset.ids[index]]);
5411         this.popup.destroy();
5412     },
5413     do_select: function(ids, records) {
5414         this._super(ids, records);
5415         this.popup.on_click_element(ids);
5416     }
5417 });
5418
5419 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5420     template: 'FieldReference',
5421     init: function(field_manager, node) {
5422         this._super(field_manager, node);
5423         this.reference_ready = true;
5424     },
5425     destroy_content: function() {
5426         if (this.fm) {
5427             this.fm.destroy();
5428             this.fm = undefined;
5429         }
5430     },
5431     initialize_content: function() {
5432         var self = this;
5433         var fm = new instance.web.form.DefaultFieldManager(this);
5434         this.fm = fm;
5435         fm.extend_field_desc({
5436             "selection": {
5437                 selection: this.field_manager.get_field_desc(this.name).selection,
5438                 type: "selection",
5439             },
5440             "m2o": {
5441                 relation: null,
5442                 type: "many2one",
5443             },
5444         });
5445         this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5446             name: 'selection',
5447             modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5448         }});
5449         this.selection.on("change:value", this, this.on_selection_changed);
5450         this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5451         this.selection
5452             .on('focused', null, function () {self.trigger('focused');})
5453             .on('blurred', null, function () {self.trigger('blurred');});
5454
5455         this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5456             name: 'Referenced Document',
5457             modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5458         }});
5459         this.m2o.on("change:value", this, this.data_changed);
5460         this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5461         this.m2o
5462             .on('focused', null, function () {self.trigger('focused');})
5463             .on('blurred', null, function () {self.trigger('blurred');});
5464     },
5465     on_selection_changed: function() {
5466         if (this.reference_ready) {
5467             this.internal_set_value([this.selection.get_value(), false]);
5468             this.render_value();
5469         }
5470     },
5471     data_changed: function() {
5472         if (this.reference_ready) {
5473             this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5474         }
5475     },
5476     set_value: function(val) {
5477         if (val) {
5478             val = val.split(',');
5479             val[0] = val[0] || false;
5480             val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5481         }
5482         this._super(val || [false, false]);
5483     },
5484     get_value: function() {
5485         return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5486     },
5487     render_value: function() {
5488         this.reference_ready = false;
5489         if (!this.get("effective_readonly")) {
5490             this.selection.set_value(this.get('value')[0]);
5491         }
5492         this.m2o.field.relation = this.get('value')[0];
5493         this.m2o.set_value(this.get('value')[1]);
5494         this.m2o.$el.toggle(!!this.get('value')[0]);
5495         this.reference_ready = true;
5496     },
5497 });
5498
5499 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5500     init: function(field_manager, node) {
5501         var self = this;
5502         this._super(field_manager, node);
5503         this.binary_value = false;
5504         this.useFileAPI = !!window.FileReader;
5505         this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5506         if (!this.useFileAPI) {
5507             this.fileupload_id = _.uniqueId('oe_fileupload');
5508             $(window).on(this.fileupload_id, function() {
5509                 var args = [].slice.call(arguments).slice(1);
5510                 self.on_file_uploaded.apply(self, args);
5511             });
5512         }
5513     },
5514     stop: function() {
5515         if (!this.useFileAPI) {
5516             $(window).off(this.fileupload_id);
5517         }
5518         this._super.apply(this, arguments);
5519     },
5520     initialize_content: function() {
5521         var self= this;
5522         this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5523         this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5524         this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5525         this.$el.find('.oe_form_binary_file_edit').click(function(event){
5526             self.$el.find('input.oe_form_binary_file').click();
5527         });
5528     },
5529     on_file_change: function(e) {
5530         var self = this;
5531         var file_node = e.target;
5532         if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5533             if (this.useFileAPI) {
5534                 var file = file_node.files[0];
5535                 if (file.size > this.max_upload_size) {
5536                     var msg = _t("The selected file exceed the maximum file size of %s.");
5537                     instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5538                     return false;
5539                 }
5540                 var filereader = new FileReader();
5541                 filereader.readAsDataURL(file);
5542                 filereader.onloadend = function(upload) {
5543                     var data = upload.target.result;
5544                     data = data.split(',')[1];
5545                     self.on_file_uploaded(file.size, file.name, file.type, data);
5546                 };
5547             } else {
5548                 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5549                 this.$el.find('form.oe_form_binary_form').submit();
5550             }
5551             this.$el.find('.oe_form_binary_progress').show();
5552             this.$el.find('.oe_form_binary').hide();
5553         }
5554     },
5555     on_file_uploaded: function(size, name, content_type, file_base64) {
5556         if (size === false) {
5557             this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5558             // TODO: use openerp web crashmanager
5559             console.warn("Error while uploading file : ", name);
5560         } else {
5561             this.filename = name;
5562             this.on_file_uploaded_and_valid.apply(this, arguments);
5563         }
5564         this.$el.find('.oe_form_binary_progress').hide();
5565         this.$el.find('.oe_form_binary').show();
5566     },
5567     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5568     },
5569     on_save_as: function(ev) {
5570         var value = this.get('value');
5571         if (!value) {
5572             this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5573             ev.stopPropagation();
5574         } else {
5575             instance.web.blockUI();
5576             var c = instance.webclient.crashmanager;
5577             this.session.get_file({
5578                 url: '/web/binary/saveas_ajax',
5579                 data: {data: JSON.stringify({
5580                     model: this.view.dataset.model,
5581                     id: (this.view.datarecord.id || ''),
5582                     field: this.name,
5583                     filename_field: (this.node.attrs.filename || ''),
5584                     data: instance.web.form.is_bin_size(value) ? null : value,
5585                     context: this.view.dataset.get_context()
5586                 })},
5587                 complete: instance.web.unblockUI,
5588                 error: c.rpc_error.bind(c)
5589             });
5590             ev.stopPropagation();
5591             return false;
5592         }
5593     },
5594     set_filename: function(value) {
5595         var filename = this.node.attrs.filename;
5596         if (filename) {
5597             var tmp = {};
5598             tmp[filename] = value;
5599             this.field_manager.set_values(tmp);
5600         }
5601     },
5602     on_clear: function() {
5603         if (this.get('value') !== false) {
5604             this.binary_value = false;
5605             this.internal_set_value(false);
5606         }
5607         return false;
5608     }
5609 });
5610
5611 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5612     template: 'FieldBinaryFile',
5613     initialize_content: function() {
5614         this._super();
5615         if (this.get("effective_readonly")) {
5616             var self = this;
5617             this.$el.find('a').click(function(ev) {
5618                 if (self.get('value')) {
5619                     self.on_save_as(ev);
5620                 }
5621                 return false;
5622             });
5623         }
5624     },
5625     render_value: function() {
5626         var show_value;
5627         if (!this.get("effective_readonly")) {
5628             if (this.node.attrs.filename) {
5629                 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5630             } else {
5631                 show_value = (this.get('value') !== null && this.get('value') !== undefined && this.get('value') !== false) ? this.get('value') : '';
5632             }
5633             this.$el.find('input').eq(0).val(show_value);
5634         } else {
5635             this.$el.find('a').toggle(!!this.get('value'));
5636             if (this.get('value')) {
5637                 show_value = _t("Download");
5638                 if (this.view)
5639                     show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5640                 this.$el.find('a').text(show_value);
5641             }
5642         }
5643     },
5644     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5645         this.binary_value = true;
5646         this.internal_set_value(file_base64);
5647         var show_value = name + " (" + instance.web.human_size(size) + ")";
5648         this.$el.find('input').eq(0).val(show_value);
5649         this.set_filename(name);
5650     },
5651     on_clear: function() {
5652         this._super.apply(this, arguments);
5653         this.$el.find('input').eq(0).val('');
5654         this.set_filename('');
5655     }
5656 });
5657
5658 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5659     template: 'FieldBinaryImage',
5660     placeholder: "/web/static/src/img/placeholder.png",
5661     render_value: function() {
5662         var self = this;
5663         var url;
5664         if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5665             url = 'data:image/png;base64,' + this.get('value');
5666         } else if (this.get('value')) {
5667             var id = JSON.stringify(this.view.datarecord.id || null);
5668             var field = this.name;
5669             if (this.options.preview_image)
5670                 field = this.options.preview_image;
5671             url = this.session.url('/web/binary/image', {
5672                                         model: this.view.dataset.model,
5673                                         id: id,
5674                                         field: field,
5675                                         t: (new Date().getTime()),
5676             });
5677         } else {
5678             url = this.placeholder;
5679         }
5680         var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5681         $($img).click(function(e) {
5682             if(self.view.get("actual_mode") == "view") {
5683                 var $button = $(".oe_form_button_edit");
5684                 $button.openerpBounce();
5685                 e.stopPropagation();
5686             }
5687         });
5688         this.$el.find('> img').remove();
5689         this.$el.prepend($img);
5690         $img.load(function() {
5691             if (! self.options.size)
5692                 return;
5693             $img.css("max-width", "" + self.options.size[0] + "px");
5694             $img.css("max-height", "" + self.options.size[1] + "px");
5695         });
5696         $img.on('error', function() {
5697             $img.attr('src', self.placeholder);
5698             instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5699         });
5700     },
5701     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5702         this.internal_set_value(file_base64);
5703         this.binary_value = true;
5704         this.render_value();
5705         this.set_filename(name);
5706     },
5707     on_clear: function() {
5708         this._super.apply(this, arguments);
5709         this.render_value();
5710         this.set_filename('');
5711     },
5712     set_value: function(value_){
5713         var changed = value_ !== this.get_value();
5714         this._super.apply(this, arguments);
5715         // By default, on binary images read, the server returns the binary size
5716         // This is possible that two images have the exact same size
5717         // Therefore we trigger the change in case the image value hasn't changed
5718         // So the image is re-rendered correctly
5719         if (!changed){
5720             this.trigger("change:value", this, {
5721                 oldValue: value_,
5722                 newValue: value_
5723             });
5724         }
5725     }
5726 });
5727
5728 /**
5729  * Widget for (many2many field) to upload one or more file in same time and display in list.
5730  * The user can delete his files.
5731  * Options on attribute ; "blockui" {Boolean} block the UI or not
5732  * during the file is uploading
5733  */
5734 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5735     template: "FieldBinaryFileUploader",
5736     init: function(field_manager, node) {
5737         this._super(field_manager, node);
5738         this.field_manager = field_manager;
5739         this.node = node;
5740         if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5741             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);
5742         }
5743         this.data = {};
5744         this.set_value([]);
5745         this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5746         this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5747         $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5748     },
5749     initialize_content: function() {
5750         this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5751     },
5752     // WARNING: duplicated in 4 other M2M widgets
5753     set_value: function(value_) {
5754         value_ = value_ || [];
5755         if (value_.length >= 1 && value_[0] instanceof Array) {
5756             // value_ is a list of m2m commands. We only process
5757             // LINK_TO and REPLACE_WITH in this context
5758             var val = [];
5759             _.each(value_, function (command) {
5760                 if (command[0] === commands.LINK_TO) {
5761                     val.push(command[1]);                   // (4, id[, _])
5762                 } else if (command[0] === commands.REPLACE_WITH) {
5763                     val = command[2];                       // (6, _, ids)
5764                 }
5765             });
5766             value_ = val;
5767         }
5768         this._super(value_);
5769     },
5770     get_value: function() {
5771         var tmp = [commands.replace_with(this.get("value"))];
5772         return tmp;
5773     },
5774     get_file_url: function (attachment) {
5775         return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5776     },
5777     read_name_values : function () {
5778         var self = this;
5779         // don't reset know values
5780         var ids = this.get('value');
5781         var _value = _.filter(ids, function (id) { return typeof self.data[id] == 'undefined'; } );
5782         // send request for get_name
5783         if (_value.length) {
5784             return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).then(function (datas) {
5785                 _.each(datas, function (data) {
5786                     data.no_unlink = true;
5787                     data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5788                     self.data[data.id] = data;
5789                 });
5790                 return ids;
5791             });
5792         } else {
5793             return $.when(ids);
5794         }
5795     },
5796     render_value: function () {
5797         var self = this;
5798         this.read_name_values().then(function (ids) {
5799             var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self, 'values': ids}));
5800             render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5801             self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5802
5803             // reinit input type file
5804             var $input = self.$('input.oe_form_binary_file');
5805             $input.after($input.clone(true)).remove();
5806             self.$(".oe_fileupload").show();
5807
5808         });
5809     },
5810     on_file_change: function (event) {
5811         event.stopPropagation();
5812         var self = this;
5813         var $target = $(event.target);
5814         if ($target.val() !== '') {
5815             var filename = $target.val().replace(/.*[\\\/]/,'');
5816             // don't uplode more of one file in same time
5817             if (self.data[0] && self.data[0].upload ) {
5818                 return false;
5819             }
5820             for (var id in this.get('value')) {
5821                 // if the files exits, delete the file before upload (if it's a new file)
5822                 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5823                     self.ds_file.unlink([id]);
5824                 }
5825             }
5826
5827             // block UI or not
5828             if(this.node.attrs.blockui>0) {
5829                 instance.web.blockUI();
5830             }
5831
5832             // TODO : unactivate send on wizard and form
5833
5834             // submit file
5835             this.$('form.oe_form_binary_form').submit();
5836             this.$(".oe_fileupload").hide();
5837             // add file on data result
5838             this.data[0] = {
5839                 'id': 0,
5840                 'name': filename,
5841                 'filename': filename,
5842                 'url': '',
5843                 'upload': true
5844             };
5845         }
5846     },
5847     on_file_loaded: function (event, result) {
5848         var files = this.get('value');
5849
5850         // unblock UI
5851         if(this.node.attrs.blockui>0) {
5852             instance.web.unblockUI();
5853         }
5854
5855         if (result.error || !result.id ) {
5856             this.do_warn( _t('Uploading Error'), result.error);
5857             delete this.data[0];
5858         } else {
5859             if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5860                 delete this.data[0];
5861                 this.data[result.id] = {
5862                     'id': result.id,
5863                     'name': result.name,
5864                     'filename': result.filename,
5865                     'url': this.get_file_url(result)
5866                 };
5867             } else {
5868                 this.data[result.id] = {
5869                     'id': result.id,
5870                     'name': result.name,
5871                     'filename': result.filename,
5872                     'url': this.get_file_url(result)
5873                 };
5874             }
5875             var values = _.clone(this.get('value'));
5876             values.push(result.id);
5877             this.set({'value': values});
5878         }
5879         this.render_value();
5880     },
5881     on_file_delete: function (event) {
5882         event.stopPropagation();
5883         var file_id=$(event.target).data("id");
5884         if (file_id) {
5885             var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5886             if(!this.data[file_id].no_unlink) {
5887                 this.ds_file.unlink([file_id]);
5888             }
5889             this.set({'value': files});
5890         }
5891     },
5892 });
5893
5894 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5895     template: "FieldStatus",
5896     init: function(field_manager, node) {
5897         this._super(field_manager, node);
5898         this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5899         this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5900         this.set({value: false});
5901         this.selection = {'unfolded': [], 'folded': []};
5902         this.set("selection", {'unfolded': [], 'folded': []});
5903         this.selection_dm = new instance.web.DropMisordered();
5904         this.dataset = new instance.web.DataSetStatic(this, this.field.relation, this.build_context());
5905     },
5906     start: function() {
5907         this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5908         this.calc_domain();
5909         this.on("change:value", this, this.get_selection);
5910         this.on("change:evaluated_selection_domain", this, this.get_selection);
5911         this.on("change:selection", this, function() {
5912             this.selection = this.get("selection");
5913             this.render_value();
5914         });
5915         this.get_selection();
5916         if (this.options.clickable) {
5917             this.$el.on('click','li[data-id]',this.on_click_stage);
5918         }
5919         if (this.$el.parent().is('header')) {
5920             this.$el.after('<div class="oe_clear"/>');
5921         }
5922         this._super();
5923     },
5924     set_value: function(value_) {
5925         if (value_ instanceof Array) {
5926             value_ = value_[0];
5927         }
5928         this._super(value_);
5929     },
5930     render_value: function() {
5931         var self = this;
5932         var content = QWeb.render("FieldStatus.content", {
5933             'widget': self, 
5934             'value_folded': _.find(self.selection.folded, function(i){return i[0] === self.get('value');})
5935         });
5936         self.$el.html(content);
5937     },
5938     calc_domain: function() {
5939         var d = instance.web.pyeval.eval('domain', this.build_domain());
5940         var domain = []; //if there is no domain defined, fetch all the records
5941
5942         if (d.length) {
5943             domain = ['|',['id', '=', this.get('value')]].concat(d);
5944         }
5945
5946         if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5947             this.set("evaluated_selection_domain", domain);
5948         }
5949     },
5950     /** Get the selection and render it
5951      *  selection: [[identifier, value_to_display], ...]
5952      *  For selection fields: this is directly given by this.field.selection
5953      *  For many2one fields:  perform a search on the relation of the many2one field
5954      */
5955     get_selection: function() {
5956         var self = this;
5957         var selection_unfolded = [];
5958         var selection_folded = [];
5959         var fold_field = this.options.fold_field;
5960
5961         var calculation = _.bind(function() {
5962             if (this.field.type == "many2one") {
5963                 return self.get_distant_fields().then(function (fields) {
5964                     return new instance.web.DataSetSearch(self, self.field.relation, self.build_context(), self.get("evaluated_selection_domain"))
5965                         .read_slice(_.union(_.keys(self.distant_fields), ['id']), {}).then(function (records) {
5966                             var ids = _.pluck(records, 'id');
5967                             return self.dataset.name_get(ids).then(function (records_name) {
5968                                 _.each(records, function (record) {
5969                                     var name = _.find(records_name, function (val) {return val[0] == record.id;})[1];
5970                                     if (fold_field && record[fold_field] && record.id != self.get('value')) {
5971                                         selection_folded.push([record.id, name]);
5972                                     } else {
5973                                         selection_unfolded.push([record.id, name]);
5974                                     }
5975                                 });
5976                             });
5977                         });
5978                     });
5979             } else {
5980                 // For field type selection filter values according to
5981                 // statusbar_visible attribute of the field. For example:
5982                 // statusbar_visible="draft,open".
5983                 var select = this.field.selection;
5984                 for(var i=0; i < select.length; i++) {
5985                     var key = select[i][0];
5986                     if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5987                         selection_unfolded.push(select[i]);
5988                     }
5989                 }
5990                 return $.when();
5991             }
5992         }, this);
5993         this.selection_dm.add(calculation()).then(function () {
5994             var selection = {'unfolded': selection_unfolded, 'folded': selection_folded};
5995             if (! _.isEqual(selection, self.get("selection"))) {
5996                 self.set("selection", selection);
5997             }
5998         });
5999     },
6000     /*
6001      * :deprecated: this feature will probably be removed with OpenERP v8
6002      */
6003     get_distant_fields: function() {
6004         var self = this;
6005         if (! this.options.fold_field) {
6006             this.distant_fields = {}
6007         }
6008         if (this.distant_fields) {
6009             return $.when(this.distant_fields);
6010         }
6011         return new instance.web.Model(self.field.relation).call("fields_get", [[this.options.fold_field]]).then(function(fields) {
6012             self.distant_fields = fields;
6013             return fields;
6014         });
6015     },
6016     on_click_stage: function (ev) {
6017         var self = this;
6018         var $li = $(ev.currentTarget);
6019         var val;
6020         if (this.field.type == "many2one") {
6021             val = parseInt($li.data("id"), 10);
6022         }
6023         else {
6024             val = $li.data("id");
6025         }
6026         if (val != self.get('value')) {
6027             this.view.recursive_save().done(function() {
6028                 var change = {};
6029                 change[self.name] = val;
6030                 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
6031                     self.view.reload();
6032                 });
6033             });
6034         }
6035     },
6036 });
6037
6038 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
6039     template: "FieldMonetary",
6040     widget_class: 'oe_form_field_float oe_form_field_monetary',
6041     init: function() {
6042         this._super.apply(this, arguments);
6043         this.set({"currency": false});
6044         if (this.options.currency_field) {
6045             this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
6046                 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
6047             });
6048         }
6049         this.on("change:currency", this, this.get_currency_info);
6050         this.get_currency_info();
6051         this.ci_dm = new instance.web.DropMisordered();
6052     },
6053     start: function() {
6054         var tmp = this._super();
6055         this.on("change:currency_info", this, this.reinitialize);
6056         return tmp;
6057     },
6058     get_currency_info: function() {
6059         var self = this;
6060         if (this.get("currency") === false) {
6061             this.set({"currency_info": null});
6062             return;
6063         }
6064         return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
6065             .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
6066             self.set({"currency_info": res});
6067         });
6068     },
6069     parse_value: function(val, def) {
6070         return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6071     },
6072     format_value: function(val, def) {
6073         return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
6074     },
6075 });
6076
6077 /*
6078     This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each
6079     record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records
6080     will be added to the relation.
6081 */
6082 instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6083     className: "oe_form_many2many_checkboxes",
6084     init: function() {
6085         this._super.apply(this, arguments);
6086         this.set("value", {});
6087         this.set("records", []);
6088         this.field_manager.on("view_content_has_changed", this, function() {
6089             var domain = new openerp.web.CompoundDomain(this.build_domain()).eval();
6090             if (! _.isEqual(domain, this.get("domain"))) {
6091                 this.set("domain", domain);
6092             }
6093         });
6094         this.records_orderer = new instance.web.DropMisordered();
6095     },
6096     initialize_field: function() {
6097         instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
6098         this.on("change:domain", this, this.query_records);
6099         this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval());
6100         this.on("change:records", this, this.render_value);
6101     },
6102     query_records: function() {
6103         var self = this;
6104         var model = new openerp.Model(openerp.session, this.field.relation);
6105         this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) {
6106             return model.call("name_get", [record_ids] , {"context": self.build_context()});
6107         })).then(function(res) {
6108             self.set("records", res);
6109         });
6110     },
6111     render_value: function() {
6112         this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")}));
6113         var inputs = this.$("input");
6114         inputs.change(_.bind(this.from_dom, this));
6115         if (this.get("effective_readonly"))
6116             inputs.attr("disabled", "true");
6117     },
6118     from_dom: function() {
6119         var new_value = {};
6120         this.$("input").each(function() {
6121             var elem = $(this);
6122             new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined;
6123         });
6124         if (! _.isEqual(new_value, this.get("value")))
6125             this.internal_set_value(new_value);
6126     },
6127     // WARNING: (mostly) duplicated in 4 other M2M widgets
6128     set_value: function(value_) {
6129         value_ = value_ || [];
6130         if (value_.length >= 1 && value_[0] instanceof Array) {
6131             // value_ is a list of m2m commands. We only process
6132             // LINK_TO and REPLACE_WITH in this context
6133             var val = [];
6134             _.each(value_, function (command) {
6135                 if (command[0] === commands.LINK_TO) {
6136                     val.push(command[1]);                   // (4, id[, _])
6137                 } else if (command[0] === commands.REPLACE_WITH) {
6138                     val = command[2];                       // (6, _, ids)
6139                 }
6140             });
6141             value_ = val;
6142         }
6143         var formatted = {};
6144         _.each(value_, function(el) {
6145             formatted[JSON.stringify(el)] = true;
6146         });
6147         this._super(formatted);
6148     },
6149     get_value: function() {
6150         var value = _.filter(_.keys(this.get("value")), function(el) {
6151             return this.get("value")[el];
6152         }, this);
6153         value = _.map(value, function(el) {
6154             return JSON.parse(el);
6155         });
6156         return [commands.replace_with(value)];
6157     },
6158 });
6159
6160 /**
6161     This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is
6162     "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window
6163     action on the model of the relation and show only the linked records.
6164
6165     Widget options:
6166
6167     * views: The views to display in the act_window action. Must be a list of tuples whose first element is the id of the view
6168       to display (or False to take the default one) and the second element is the type of the view. Defaults to
6169       [[false, "tree"], [false, "form"]] .
6170 */
6171 instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
6172     className: "oe_form_x2many_counter",
6173     init: function() {
6174         this._super.apply(this, arguments);
6175         this.set("value", []);
6176         _.defaults(this.options, {
6177             "views": [[false, "tree"], [false, "form"]],
6178         });
6179     },
6180     render_value: function() {
6181         var text = _.str.sprintf("%d %s", this.val().length, this.string);
6182         this.$().html(QWeb.render("X2ManyCounter", {text: text}));
6183         this.$("a").click(_.bind(this.go_to, this));
6184     },
6185     go_to: function() {
6186         return this.view.recursive_save().then(_.bind(function() {
6187             var val = this.val();
6188             var context = {};
6189             if (this.field.type === "one2many") {
6190                 context["default_" + this.field.relation_field] = this.view.datarecord.id;
6191             }
6192             var domain = [["id", "in", val]];
6193             return this.do_action({
6194                 type: 'ir.actions.act_window',
6195                 name: this.string,
6196                 res_model: this.field.relation,
6197                 views: this.options.views,
6198                 target: 'current',
6199                 context: context,
6200                 domain: domain,
6201             });
6202         }, this));
6203     },
6204     val: function() {
6205         var value = this.get("value") || [];
6206         if (value.length >= 1 && value[0] instanceof Array) {
6207             value = value[0][2];
6208         }
6209         return value;
6210     }
6211 });
6212
6213 /**
6214     This widget is intended to be used on stat button numeric fields.  It will display
6215     the value   many2many and one2many. It is a read-only field that will 
6216     display a simple string "<value of field> <label of the field>"
6217 */
6218 instance.web.form.StatInfo = instance.web.form.AbstractField.extend({
6219     is_field_number: true,
6220     init: function() {
6221         this._super.apply(this, arguments);
6222         this.internal_set_value(0);
6223     },
6224     set_value: function(value_) {
6225         if (value_ === false || value_ === undefined) {
6226             value_ = 0;
6227         }
6228         this._super.apply(this, [value_]);
6229     },
6230     render_value: function() {
6231         var options = {
6232             value: this.get("value") || 0,
6233         };
6234         if (! this.node.attrs.nolabel) {
6235             if(this.options.label_field && this.view.datarecord[this.options.label_field]) {
6236                 options.text = this.view.datarecord[this.options.label_field];
6237             }
6238             else {
6239                 options.text = this.string;
6240             }
6241         }
6242         this.$el.html(QWeb.render("StatInfo", options));
6243     },
6244
6245 });
6246
6247
6248 /**
6249  * Registry of form fields, called by :js:`instance.web.FormView`.
6250  *
6251  * All referenced classes must implement FieldInterface. Those represent the classes whose instances
6252  * will substitute to the <field> tags as defined in OpenERP's views.
6253  */
6254 instance.web.form.widgets = new instance.web.Registry({
6255     'char' : 'instance.web.form.FieldChar',
6256     'id' : 'instance.web.form.FieldID',
6257     'email' : 'instance.web.form.FieldEmail',
6258     'url' : 'instance.web.form.FieldUrl',
6259     'text' : 'instance.web.form.FieldText',
6260     'html' : 'instance.web.form.FieldTextHtml',
6261     'char_domain': 'instance.web.form.FieldCharDomain',
6262     'date' : 'instance.web.form.FieldDate',
6263     'datetime' : 'instance.web.form.FieldDatetime',
6264     'selection' : 'instance.web.form.FieldSelection',
6265     'radio' : 'instance.web.form.FieldRadio',
6266     'many2one' : 'instance.web.form.FieldMany2One',
6267     'many2onebutton' : 'instance.web.form.Many2OneButton',
6268     'many2many' : 'instance.web.form.FieldMany2Many',
6269     'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
6270     'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
6271     'one2many' : 'instance.web.form.FieldOne2Many',
6272     'one2many_list' : 'instance.web.form.FieldOne2Many',
6273     'reference' : 'instance.web.form.FieldReference',
6274     'boolean' : 'instance.web.form.FieldBoolean',
6275     'float' : 'instance.web.form.FieldFloat',
6276     'percentpie': 'instance.web.form.FieldPercentPie',
6277     'barchart': 'instance.web.form.FieldBarChart',
6278     'integer': 'instance.web.form.FieldFloat',
6279     'float_time': 'instance.web.form.FieldFloat',
6280     'progressbar': 'instance.web.form.FieldProgressBar',
6281     'image': 'instance.web.form.FieldBinaryImage',
6282     'binary': 'instance.web.form.FieldBinaryFile',
6283     'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
6284     'statusbar': 'instance.web.form.FieldStatus',
6285     'monetary': 'instance.web.form.FieldMonetary',
6286     'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes',
6287     'x2many_counter': 'instance.web.form.X2ManyCounter',
6288     'priority':'instance.web.form.Priority',
6289     'kanban_state_selection':'instance.web.form.KanbanSelection',
6290     'statinfo': 'instance.web.form.StatInfo',
6291 });
6292
6293 /**
6294  * Registry of widgets usable in the form view that can substitute to any possible
6295  * tags defined in OpenERP's form views.
6296  *
6297  * Every referenced class should extend FormWidget.
6298  */
6299 instance.web.form.tags = new instance.web.Registry({
6300     'button' : 'instance.web.form.WidgetButton',
6301 });
6302
6303 instance.web.form.custom_widgets = new instance.web.Registry({
6304 });
6305
6306 })();
6307
6308 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: