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