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