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