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