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