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