[MERGE] forward port of branch 7.0 up to revid 4067 chs@openerp.com-20131114142639...
[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 instance.web.form.FieldRadio = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
2866     template: 'FieldRadio',
2867     events: {
2868         'click input': 'click_change_value'
2869     },
2870     init: function(field_manager, node) {
2871         /* Radio button widget: Attributes options:
2872         * - "horizontal" to display in column
2873         * - "no_radiolabel" don't display text values
2874         */
2875         this._super(field_manager, node);
2876         this.selection = _.clone(this.field.selection) || [];
2877         this.domain = false;
2878     },
2879     initialize_content: function () {
2880         this.uniqueId = _.uniqueId("radio");
2881         this.on("change:effective_readonly", this, this.render_value);
2882         this.field_manager.on("view_content_has_changed", this, this.get_selection);
2883         this.get_selection();
2884     },
2885     click_change_value: function (event) {
2886         var val = $(event.target).val();
2887         val = this.field.type == "selection" ? val : +val;
2888         if (val == this.get_value()) {
2889             this.set_value(false);
2890         } else {
2891             this.set_value(val);
2892         }
2893     },
2894     /** Get the selection and render it
2895      *  selection: [[identifier, value_to_display], ...]
2896      *  For selection fields: this is directly given by this.field.selection
2897      *  For many2one fields:  perform a search on the relation of the many2one field
2898      */
2899     get_selection: function() {
2900         var self = this;
2901         var selection = [];
2902         var def = $.Deferred();
2903         if (self.field.type == "many2one") {
2904             var domain = instance.web.pyeval.eval('domain', this.build_domain()) || [];
2905             if (! _.isEqual(self.domain, domain)) {
2906                 self.domain = domain;
2907                 var ds = new instance.web.DataSetStatic(self, self.field.relation, self.build_context());
2908                 ds.call('search', [self.domain])
2909                     .then(function (records) {
2910                         ds.name_get(records).then(function (records) {
2911                             selection = records;
2912                             def.resolve();
2913                         });
2914                     });
2915             } else {
2916                 selection = self.selection;
2917                 def.resolve();
2918             }
2919         }
2920         else if (self.field.type == "selection") {
2921             selection = self.field.selection || [];
2922             def.resolve();
2923         }
2924         return def.then(function () {
2925             if (! _.isEqual(selection, self.selection)) {
2926                 self.selection = _.clone(selection);
2927                 self.renderElement();
2928                 self.render_value();
2929             }
2930         });
2931     },
2932     set_value: function (value_) {
2933         if (value_) {
2934             if (this.field.type == "selection") {
2935                 value_ = _.find(this.field.selection, function (sel) { return sel[0] == value_});
2936             }
2937             else if (!this.selection.length) {
2938                 this.selection = [value_];
2939             }
2940         }
2941         this._super(value_);
2942     },
2943     get_value: function () {
2944         var value = this.get('value');
2945         return value instanceof Array ? value[0] : value;
2946     },
2947     render_value: function () {
2948         var self = this;
2949         this.$el.toggleClass("oe_readonly", this.get('effective_readonly'));
2950         this.$("input:checked").prop("checked", false);
2951         if (this.get_value()) {
2952             this.$("input").filter(function () {return this.value == self.get_value()}).prop("checked", true);
2953             this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : "");
2954         }
2955     }
2956 });
2957
2958 // jquery autocomplete tweak to allow html and classnames
2959 (function() {
2960     var proto = $.ui.autocomplete.prototype,
2961         initSource = proto._initSource;
2962
2963     function filter( array, term ) {
2964         var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
2965         return $.grep( array, function(value_) {
2966             return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
2967         });
2968     }
2969
2970     $.extend( proto, {
2971         _initSource: function() {
2972             if ( this.options.html && $.isArray(this.options.source) ) {
2973                 this.source = function( request, response ) {
2974                     response( filter( this.options.source, request.term ) );
2975                 };
2976             } else {
2977                 initSource.call( this );
2978             }
2979         },
2980
2981         _renderItem: function( ul, item) {
2982             return $( "<li></li>" )
2983                 .data( "item.autocomplete", item )
2984                 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
2985                 .appendTo( ul )
2986                 .addClass(item.classname);
2987         }
2988     });
2989 })();
2990
2991 /**
2992  * A mixin containing some useful methods to handle completion inputs.
2993  */
2994 instance.web.form.CompletionFieldMixin = {
2995     init: function() {
2996         this.limit = 7;
2997         this.orderer = new instance.web.DropMisordered();
2998     },
2999     /**
3000      * Call this method to search using a string.
3001      */
3002     get_search_result: function(search_val) {
3003         var self = this;
3004
3005         var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
3006         var blacklist = this.get_search_blacklist();
3007         this.last_query = search_val;
3008
3009         return this.orderer.add(dataset.name_search(
3010                 search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
3011                 'ilike', this.limit + 1, self.build_context())).then(function(data) {
3012             self.last_search = data;
3013             // possible selections for the m2o
3014             var values = _.map(data, function(x) {
3015                 x[1] = x[1].split("\n")[0];
3016                 return {
3017                     label: _.str.escapeHTML(x[1]),
3018                     value: x[1],
3019                     name: x[1],
3020                     id: x[0],
3021                 };
3022             });
3023
3024             // search more... if more results that max
3025             if (values.length > self.limit) {
3026                 values = values.slice(0, self.limit);
3027                 values.push({
3028                     label: _t("Search More..."),
3029                     action: function() {
3030                         dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
3031                             self._search_create_popup("search", data);
3032                         });
3033                     },
3034                     classname: 'oe_m2o_dropdown_option'
3035                 });
3036             }
3037             // quick create
3038             var raw_result = _(data.result).map(function(x) {return x[1];});
3039             if (search_val.length > 0 && !_.include(raw_result, search_val)) {
3040                 values.push({
3041                     label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
3042                         $('<span />').text(search_val).html()),
3043                     action: function() {
3044                         self._quick_create(search_val);
3045                     },
3046                     classname: 'oe_m2o_dropdown_option'
3047                 });
3048             }
3049             // create...
3050             values.push({
3051                 label: _t("Create and Edit..."),
3052                 action: function() {
3053                     self._search_create_popup("form", undefined, self._create_context(search_val));
3054                 },
3055                 classname: 'oe_m2o_dropdown_option'
3056             });
3057
3058             return values;
3059         });
3060     },
3061     get_search_blacklist: function() {
3062         return [];
3063     },
3064     _quick_create: function(name) {
3065         var self = this;
3066         var slow_create = function () {
3067             self._search_create_popup("form", undefined, self._create_context(name));
3068         };
3069         if (self.options.quick_create === undefined || self.options.quick_create) {
3070             new instance.web.DataSet(this, this.field.relation, self.build_context())
3071                 .name_create(name).done(function(data) {
3072                     self.add_id(data[0]);
3073                 }).fail(function(error, event) {
3074                     event.preventDefault();
3075                     slow_create();
3076                 });
3077         } else
3078             slow_create();
3079     },
3080     // all search/create popup handling
3081     _search_create_popup: function(view, ids, context) {
3082         var self = this;
3083         var pop = new instance.web.form.SelectCreatePopup(this);
3084         pop.select_element(
3085             self.field.relation,
3086             {
3087                 title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
3088                 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
3089                 initial_view: view,
3090                 disable_multiple_selection: true
3091             },
3092             self.build_domain(),
3093             new instance.web.CompoundContext(self.build_context(), context || {})
3094         );
3095         pop.on("elements_selected", self, function(element_ids) {
3096             self.add_id(element_ids[0]);
3097             self.focus();
3098         });
3099     },
3100     /**
3101      * To implement.
3102      */
3103     add_id: function(id) {},
3104     _create_context: function(name) {
3105         var tmp = {};
3106         var field = (this.options || {}).create_name_field;
3107         if (field === undefined)
3108             field = "name";
3109         if (field !== false && name && (this.options || {}).quick_create !== false)
3110             tmp["default_" + field] = name;
3111         return tmp;
3112     },
3113 };
3114
3115 instance.web.form.M2ODialog = instance.web.Dialog.extend({
3116     template: "M2ODialog",
3117     init: function(parent) {
3118         this._super(parent, {
3119             title: _.str.sprintf(_t("Add %s"), parent.string),
3120             width: 312,
3121         });
3122     },
3123     start: function() {
3124         var self = this;
3125         this.$buttons.html(QWeb.render("M2ODialog.buttons"));
3126         this.$("input").val(this.getParent().last_query);
3127         this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
3128             self.getParent()._quick_create(self.$("input").val());
3129             self.destroy();
3130         });
3131         this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
3132             self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
3133             self.destroy();
3134         });
3135         this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
3136             self.destroy();
3137         });
3138     },
3139 });
3140
3141 instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
3142     template: "FieldMany2One",
3143     events: {
3144         'keydown input': function (e) {
3145             switch (e.which) {
3146             case $.ui.keyCode.UP:
3147             case $.ui.keyCode.DOWN:
3148                 e.stopPropagation();
3149             }
3150         },
3151     },
3152     init: function(field_manager, node) {
3153         this._super(field_manager, node);
3154         instance.web.form.CompletionFieldMixin.init.call(this);
3155         this.set({'value': false});
3156         this.display_value = {};
3157         this.display_value_backup = {};
3158         this.last_search = [];
3159         this.floating = false;
3160         this.current_display = null;
3161         this.is_started = false;
3162     },
3163     reinit_value: function(val) {
3164         this.internal_set_value(val);
3165         this.floating = false;
3166         if (this.is_started)
3167             this.render_value();
3168     },
3169     initialize_field: function() {
3170         this.is_started = true;
3171         instance.web.bus.on('click', this, function() {
3172             if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
3173                 this.$input.autocomplete("close");
3174             }
3175         });
3176         instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
3177     },
3178     initialize_content: function() {
3179         if (!this.get("effective_readonly"))
3180             this.render_editable();
3181     },
3182     destroy_content: function () {
3183         if (this.$drop_down) {
3184             this.$drop_down.off('click');
3185             delete this.$drop_down;
3186         }
3187         if (this.$input) {
3188             this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
3189             this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
3190                             'focus focusout change keydown');
3191             delete this.$input;
3192         }
3193         if (this.$follow_button) {
3194             this.$follow_button.off('blur focus click');
3195             delete this.$follow_button;
3196         }
3197     },
3198     destroy: function () {
3199         this.destroy_content();
3200         return this._super();
3201     },
3202     init_error_displayer: function() {
3203         // nothing
3204     },
3205     hide_error_displayer: function() {
3206         // doesn't work
3207     },
3208     show_error_displayer: function() {
3209         new instance.web.form.M2ODialog(this).open();
3210     },
3211     render_editable: function() {
3212         var self = this;
3213         this.$input = this.$el.find("input");
3214
3215         this.init_error_displayer();
3216
3217         self.$input.on('focus', function() {
3218             self.hide_error_displayer();
3219         });
3220
3221         this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
3222         this.$follow_button = $(".oe_m2o_cm_button", this.$el);
3223
3224         this.$follow_button.click(function(ev) {
3225             ev.preventDefault();
3226             if (!self.get('value')) {
3227                 self.focus();
3228                 return;
3229             }
3230             var pop = new instance.web.form.FormOpenPopup(self);
3231             pop.show_element(
3232                 self.field.relation,
3233                 self.get("value"),
3234                 self.build_context(),
3235                 {
3236                     title: _t("Open: ") + self.string
3237                 }
3238             );
3239             pop.on('write_completed', self, function(){
3240                 self.display_value = {};
3241                 self.display_value_backup = {};
3242                 self.render_value();
3243                 self.focus();
3244                 self.view.do_onchange(self);
3245             });
3246         });
3247
3248         // some behavior for input
3249         var input_changed = function() {
3250             if (self.current_display !== self.$input.val()) {
3251                 self.current_display = self.$input.val();
3252                 if (self.$input.val() === "") {
3253                     self.internal_set_value(false);
3254                     self.floating = false;
3255                 } else {
3256                     self.floating = true;
3257                 }
3258             }
3259         };
3260         this.$input.keydown(input_changed);
3261         this.$input.change(input_changed);
3262         this.$drop_down.click(function() {
3263                 self.$input.focus();
3264             if (self.$input.autocomplete("widget").is(":visible")) {
3265                 self.$input.autocomplete("close");                
3266             } else {
3267                 if (self.get("value") && ! self.floating) {
3268                     self.$input.autocomplete("search", "");
3269                 } else {
3270                     self.$input.autocomplete("search");
3271                 }
3272             }
3273         });
3274
3275         // Autocomplete close on dialog content scroll
3276         var close_autocomplete = _.debounce(function() {
3277             if (self.$input.autocomplete("widget").is(":visible")) {
3278                 self.$input.autocomplete("close");
3279             }
3280         }, 50);
3281         this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
3282
3283         self.ed_def = $.Deferred();
3284         self.uned_def = $.Deferred();
3285         var ed_delay = 200;
3286         var ed_duration = 15000;
3287         var anyoneLoosesFocus = function (e) {
3288             var used = false;
3289             if (self.floating) {
3290                 if (self.last_search.length > 0) {
3291                     if (self.last_search[0][0] != self.get("value")) {
3292                         self.display_value = {};
3293                         self.display_value_backup = {};
3294                         self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
3295                         self.reinit_value(self.last_search[0][0]);
3296                     } else {
3297                         used = true;
3298                         self.render_value();
3299                     }
3300                 } else {
3301                     used = true;
3302                     self.reinit_value(false);
3303                 }
3304                 self.floating = false;
3305             }
3306             if (used && self.get("value") === false && ! self.no_ed) {
3307                 self.ed_def.reject();
3308                 self.uned_def.reject();
3309                 self.ed_def = $.Deferred();
3310                 self.ed_def.done(function() {
3311                     self.show_error_displayer();
3312                     ignore_blur = false;
3313                     self.trigger('focused');
3314                 });
3315                 ignore_blur = true;
3316                 setTimeout(function() {
3317                     self.ed_def.resolve();
3318                     self.uned_def.reject();
3319                     self.uned_def = $.Deferred();
3320                     self.uned_def.done(function() {
3321                         self.hide_error_displayer();
3322                     });
3323                     setTimeout(function() {self.uned_def.resolve();}, ed_duration);
3324                 }, ed_delay);
3325             } else {
3326                 self.no_ed = false;
3327                 self.ed_def.reject();
3328             }
3329         };
3330         var ignore_blur = false;
3331         this.$input.on({
3332             focusout: anyoneLoosesFocus,
3333             focus: function () { self.trigger('focused'); },
3334             autocompleteopen: function () { ignore_blur = true; },
3335             autocompleteclose: function () { ignore_blur = false; },
3336             blur: function () {
3337                 // autocomplete open
3338                 if (ignore_blur) { return; }
3339                 if (_(self.getChildren()).any(function (child) {
3340                     return child instanceof instance.web.form.AbstractFormPopup;
3341                 })) { return; }
3342                 self.trigger('blurred');
3343             }
3344         });
3345
3346         var isSelecting = false;
3347         // autocomplete
3348         this.$input.autocomplete({
3349             source: function(req, resp) {
3350                 self.get_search_result(req.term).done(function(result) {
3351                     resp(result);
3352                 });
3353             },
3354             select: function(event, ui) {
3355                 isSelecting = true;
3356                 var item = ui.item;
3357                 if (item.id) {
3358                     self.display_value = {};
3359                     self.display_value_backup = {};
3360                     self.display_value["" + item.id] = item.name;
3361                     self.reinit_value(item.id);
3362                 } else if (item.action) {
3363                     item.action();
3364                     // Cancel widget blurring, to avoid form blur event
3365                     self.trigger('focused');
3366                     return false;
3367                 }
3368             },
3369             focus: function(e, ui) {
3370                 e.preventDefault();
3371             },
3372             html: true,
3373             // disabled to solve a bug, but may cause others
3374             //close: anyoneLoosesFocus,
3375             minLength: 0,
3376             delay: 0
3377         });
3378         this.$input.autocomplete("widget").openerpClass();
3379         // used to correct a bug when selecting an element by pushing 'enter' in an editable list
3380         this.$input.keyup(function(e) {
3381             if (e.which === 13) { // ENTER
3382                 if (isSelecting)
3383                     e.stopPropagation();
3384             }
3385             isSelecting = false;
3386         });
3387         this.setupFocus(this.$follow_button);
3388     },
3389     render_value: function(no_recurse) {
3390         var self = this;
3391         if (! this.get("value")) {
3392             this.display_string("");
3393             return;
3394         }
3395         var display = this.display_value["" + this.get("value")];
3396         if (display) {
3397             this.display_string(display);
3398             return;
3399         }
3400         if (! no_recurse) {
3401             var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
3402             this.alive(dataset.name_get([self.get("value")])).done(function(data) {
3403                 self.display_value["" + self.get("value")] = data[0][1];
3404                 self.render_value(true);
3405             }).fail( function (data, event) {
3406                 // avoid displaying crash errors as many2One should be name_get compliant
3407                 event.preventDefault();
3408                 self.display_value["" + self.get("value")] = self.display_value_backup["" + self.get("value")];
3409                 self.render_value(true);
3410             });
3411         }
3412     },
3413     display_string: function(str) {
3414         var self = this;
3415         if (!this.get("effective_readonly")) {
3416             this.$input.val(str.split("\n")[0]);
3417             this.current_display = this.$input.val();
3418             if (this.is_false()) {
3419                 this.$('.oe_m2o_cm_button').css({'display':'none'});
3420             } else {
3421                 this.$('.oe_m2o_cm_button').css({'display':'inline'});
3422             }
3423         } else {
3424             var lines = _.escape(str).split("\n");
3425             var link = "";
3426             var follow = "";
3427             link = lines[0];
3428             follow = _.rest(lines).join("<br />");
3429             if (follow)
3430                 link += "<br />";
3431             var $link = this.$el.find('.oe_form_uri')
3432                  .unbind('click')
3433                  .html(link);
3434             if (! this.options.no_open)
3435                 $link.click(function () {
3436                     self.do_action({
3437                         type: 'ir.actions.act_window',
3438                         res_model: self.field.relation,
3439                         res_id: self.get("value"),
3440                         views: [[false, 'form']],
3441                         target: 'current',
3442                         context: self.build_context().eval(),
3443                     });
3444                     return false;
3445                  });
3446             $(".oe_form_m2o_follow", this.$el).html(follow);
3447         }
3448     },
3449     set_value: function(value_) {
3450         var self = this;
3451         if (value_ instanceof Array) {
3452             this.display_value = {};
3453             this.display_value_backup = {}
3454             if (! this.options.always_reload) {
3455                 this.display_value["" + value_[0]] = value_[1];
3456             }
3457             else {
3458                 this.display_value_backup["" + value_[0]] = value_[1];
3459             }
3460             value_ = value_[0];
3461         }
3462         value_ = value_ || false;
3463         this.reinit_value(value_);
3464     },
3465     get_displayed: function() {
3466         return this.display_value["" + this.get("value")];
3467     },
3468     add_id: function(id) {
3469         this.display_value = {};
3470         this.display_value_backup = {};
3471         this.reinit_value(id);
3472     },
3473     is_false: function() {
3474         return ! this.get("value");
3475     },
3476     focus: function () {
3477         var input = !this.get('effective_readonly') && this.$input && this.$input[0];
3478         return input ? input.focus() : false;
3479     },
3480     _quick_create: function() {
3481         this.no_ed = true;
3482         this.ed_def.reject();
3483         return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
3484     },
3485     _search_create_popup: function() {
3486         this.no_ed = true;
3487         this.ed_def.reject();
3488         return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
3489     },
3490     set_dimensions: function (height, width) {
3491         this._super(height, width);
3492         this.$input.css('height', height);
3493     }
3494 });
3495
3496 instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
3497     template: 'Many2OneButton',
3498     init: function(field_manager, node) {
3499         this._super.apply(this, arguments);
3500     },
3501     start: function() {
3502         this._super.apply(this, arguments);
3503         this.set_button();
3504     },
3505     set_button: function() {
3506         var self = this;
3507         if (this.$button) {
3508             this.$button.remove();
3509         }
3510         this.string = '';
3511         this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
3512         this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
3513         this.$button.addClass('oe_link').css({'padding':'4px'});
3514         this.$el.append(this.$button);
3515         this.$button.on('click', self.on_click);
3516     },
3517     on_click: function(ev) {
3518         var self = this;
3519         this.popup =  new instance.web.form.FormOpenPopup(this);
3520         this.popup.show_element(
3521             this.field.relation,
3522             this.get('value'),
3523             this.build_context(),
3524             {title: this.string}
3525         );
3526         this.popup.on('create_completed', self, function(r) {
3527             self.set_value(r);
3528         });
3529     },
3530     set_value: function(value_) {
3531         var self = this;
3532         if (value_ instanceof Array) {
3533             value_ = value_[0];
3534         }
3535         value_ = value_ || false;
3536         this.set('value', value_);
3537         this.set_button();
3538      },
3539 });
3540
3541 /*
3542 # Values: (0, 0,  { fields })    create
3543 #         (1, ID, { fields })    update
3544 #         (2, ID)                remove (delete)
3545 #         (3, ID)                unlink one (target id or target of relation)
3546 #         (4, ID)                link
3547 #         (5)                    unlink all (only valid for one2many)
3548 */
3549 var commands = {
3550     // (0, _, {values})
3551     CREATE: 0,
3552     'create': function (values) {
3553         return [commands.CREATE, false, values];
3554     },
3555     // (1, id, {values})
3556     UPDATE: 1,
3557     'update': function (id, values) {
3558         return [commands.UPDATE, id, values];
3559     },
3560     // (2, id[, _])
3561     DELETE: 2,
3562     'delete': function (id) {
3563         return [commands.DELETE, id, false];
3564     },
3565     // (3, id[, _]) removes relation, but not linked record itself
3566     FORGET: 3,
3567     'forget': function (id) {
3568         return [commands.FORGET, id, false];
3569     },
3570     // (4, id[, _])
3571     LINK_TO: 4,
3572     'link_to': function (id) {
3573         return [commands.LINK_TO, id, false];
3574     },
3575     // (5[, _[, _]])
3576     DELETE_ALL: 5,
3577     'delete_all': function () {
3578         return [5, false, false];
3579     },
3580     // (6, _, ids) replaces all linked records with provided ids
3581     REPLACE_WITH: 6,
3582     'replace_with': function (ids) {
3583         return [6, false, ids];
3584     }
3585 };
3586 instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
3587     multi_selection: false,
3588     disable_utility_classes: true,
3589     init: function(field_manager, node) {
3590         this._super(field_manager, node);
3591         lazy_build_o2m_kanban_view();
3592         this.is_loaded = $.Deferred();
3593         this.initial_is_loaded = this.is_loaded;
3594         this.form_last_update = $.Deferred();
3595         this.init_form_last_update = this.form_last_update;
3596         this.is_started = false;
3597         this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
3598         this.dataset.o2m = this;
3599         this.dataset.parent_view = this.view;
3600         this.dataset.child_name = this.name;
3601         var self = this;
3602         this.dataset.on('dataset_changed', this, function() {
3603             self.trigger_on_change();
3604         });
3605         this.set_value([]);
3606     },
3607     start: function() {
3608         this._super.apply(this, arguments);
3609         this.$el.addClass('oe_form_field oe_form_field_one2many');
3610
3611         var self = this;
3612
3613         self.load_views();
3614         this.is_loaded.done(function() {
3615             self.on("change:effective_readonly", self, function() {
3616                 self.is_loaded = self.is_loaded.then(function() {
3617                     self.viewmanager.destroy();
3618                     return $.when(self.load_views()).done(function() {
3619                         self.reload_current_view();
3620                     });
3621                 });
3622             });
3623         });
3624         this.is_started = true;
3625         this.reload_current_view();
3626     },
3627     trigger_on_change: function() {
3628         this.trigger('changed_value');
3629     },
3630     load_views: function() {
3631         var self = this;
3632
3633         var modes = this.node.attrs.mode;
3634         modes = !!modes ? modes.split(",") : ["tree"];
3635         var views = [];
3636         _.each(modes, function(mode) {
3637             if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
3638                 throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
3639             }
3640             var view = {
3641                 view_id: false,
3642                 view_type: mode == "tree" ? "list" : mode,
3643                 options: {}
3644             };
3645             if (self.field.views && self.field.views[mode]) {
3646                 view.embedded_view = self.field.views[mode];
3647             }
3648             if(view.view_type === "list") {
3649                 _.extend(view.options, {
3650                     addable: null,
3651                     selectable: self.multi_selection,
3652                     sortable: true,
3653                     import_enabled: false,
3654                     deletable: true
3655                 });
3656                 if (self.get("effective_readonly")) {
3657                     _.extend(view.options, {
3658                         deletable: null,
3659                         reorderable: false,
3660                     });
3661                 }
3662             } else if (view.view_type === "form") {
3663                 if (self.get("effective_readonly")) {
3664                     view.view_type = 'form';
3665                 }
3666                 _.extend(view.options, {
3667                     not_interactible_on_create: true,
3668                 });
3669             } else if (view.view_type === "kanban") {
3670                 _.extend(view.options, {
3671                     confirm_on_delete: false,
3672                 });
3673                 if (self.get("effective_readonly")) {
3674                     _.extend(view.options, {
3675                         action_buttons: false,
3676                         quick_creatable: false,
3677                         creatable: false,
3678                         read_only_mode: true,
3679                     });
3680                 }
3681             }
3682             views.push(view);
3683         });
3684         this.views = views;
3685
3686         this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
3687         this.viewmanager.o2m = self;
3688         var once = $.Deferred().done(function() {
3689             self.init_form_last_update.resolve();
3690         });
3691         var def = $.Deferred().done(function() {
3692             self.initial_is_loaded.resolve();
3693         });
3694         this.viewmanager.on("controller_inited", self, function(view_type, controller) {
3695             controller.o2m = self;
3696             if (view_type == "list") {
3697                 if (self.get("effective_readonly")) {
3698                     controller.on('edit:before', self, function (e) {
3699                         e.cancel = true;
3700                     });
3701                     _(controller.columns).find(function (column) {
3702                         if (!(column instanceof instance.web.list.Handle)) {
3703                             return false;
3704                         }
3705                         column.modifiers.invisible = true;
3706                         return true;
3707                     });
3708                 }
3709             } else if (view_type === "form") {
3710                 if (self.get("effective_readonly")) {
3711                     $(".oe_form_buttons", controller.$el).children().remove();
3712                 }
3713                 controller.on("load_record", self, function(){
3714                      once.resolve();
3715                  });
3716                 controller.on('pager_action_executed',self,self.save_any_view);
3717             } else if (view_type == "graph") {
3718                 self.reload_current_view()
3719             }
3720             def.resolve();
3721         });
3722         this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
3723             $.when(self.save_any_view()).done(function() {
3724                 if (n_mode === "list") {
3725                     $.async_when().done(function() {
3726                         self.reload_current_view();
3727                     });
3728                 }
3729             });
3730         });
3731         $.async_when().done(function () {
3732             self.viewmanager.appendTo(self.$el);
3733         });
3734         return def;
3735     },
3736     reload_current_view: function() {
3737         var self = this;
3738         return self.is_loaded = self.is_loaded.then(function() {
3739             var active_view = self.viewmanager.active_view;
3740             var view = self.viewmanager.views[active_view].controller;
3741             if(active_view === "list") {
3742                 return view.reload_content();
3743             } else if (active_view === "form") {
3744                 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
3745                     self.dataset.index = 0;
3746                 }
3747                 var act = function() {
3748                     return view.do_show();
3749                 };
3750                 self.form_last_update = self.form_last_update.then(act, act);
3751                 return self.form_last_update;
3752             } else if (view.do_search) {
3753                 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
3754             }
3755         }, undefined);
3756     },
3757     set_value: function(value_) {
3758         value_ = value_ || [];
3759         var self = this;
3760         this.dataset.reset_ids([]);
3761         if(value_.length >= 1 && value_[0] instanceof Array) {
3762             var ids = [];
3763             _.each(value_, function(command) {
3764                 var obj = {values: command[2]};
3765                 switch (command[0]) {
3766                     case commands.CREATE:
3767                         obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3768                         obj.defaults = {};
3769                         self.dataset.to_create.push(obj);
3770                         self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3771                         ids.push(obj.id);
3772                         return;
3773                     case commands.UPDATE:
3774                         obj['id'] = command[1];
3775                         self.dataset.to_write.push(obj);
3776                         self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
3777                         ids.push(obj.id);
3778                         return;
3779                     case commands.DELETE:
3780                         self.dataset.to_delete.push({id: command[1]});
3781                         return;
3782                     case commands.LINK_TO:
3783                         ids.push(command[1]);
3784                         return;
3785                     case commands.DELETE_ALL:
3786                         self.dataset.delete_all = true;
3787                         return;
3788                 }
3789             });
3790             this._super(ids);
3791             this.dataset.set_ids(ids);
3792         } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
3793             var ids = [];
3794             this.dataset.delete_all = true;
3795             _.each(value_, function(command) {
3796                 var obj = {values: command};
3797                 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
3798                 obj.defaults = {};
3799                 self.dataset.to_create.push(obj);
3800                 self.dataset.cache.push(_.clone(obj));
3801                 ids.push(obj.id);
3802             });
3803             this._super(ids);
3804             this.dataset.set_ids(ids);
3805         } else {
3806             this._super(value_);
3807             this.dataset.reset_ids(value_);
3808         }
3809         if (this.dataset.index === null && this.dataset.ids.length > 0) {
3810             this.dataset.index = 0;
3811         }
3812         this.trigger_on_change();
3813         if (this.is_started) {
3814             return self.reload_current_view();
3815         } else {
3816             return $.when();
3817         }
3818     },
3819     get_value: function() {
3820         var self = this;
3821         if (!this.dataset)
3822             return [];
3823         var val = this.dataset.delete_all ? [commands.delete_all()] : [];
3824         val = val.concat(_.map(this.dataset.ids, function(id) {
3825             var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
3826             if (alter_order) {
3827                 return commands.create(alter_order.values);
3828             }
3829             alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
3830             if (alter_order) {
3831                 return commands.update(alter_order.id, alter_order.values);
3832             }
3833             return commands.link_to(id);
3834         }));
3835         return val.concat(_.map(
3836             this.dataset.to_delete, function(x) {
3837                 return commands['delete'](x.id);}));
3838     },
3839     commit_value: function() {
3840         return this.save_any_view();
3841     },
3842     save_any_view: function() {
3843         if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
3844             this.viewmanager.views[this.viewmanager.active_view] &&
3845             this.viewmanager.views[this.viewmanager.active_view].controller) {
3846             var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3847             if (this.viewmanager.active_view === "form") {
3848                 if (!view.is_initialized.state() === 'resolved') {
3849                     return $.when(false);
3850                 }
3851                 return $.when(view.save());
3852             } else if (this.viewmanager.active_view === "list") {
3853                 return $.when(view.ensure_saved());
3854             }
3855         }
3856         return $.when(false);
3857     },
3858     is_syntax_valid: function() {
3859         if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
3860             return true;
3861         var view = this.viewmanager.views[this.viewmanager.active_view].controller;
3862         switch (this.viewmanager.active_view) {
3863         case 'form':
3864             return _(view.fields).chain()
3865                 .invoke('is_valid')
3866                 .all(_.identity)
3867                 .value();
3868             break;
3869         case 'list':
3870             return view.is_valid();
3871         }
3872         return true;
3873     },
3874 });
3875
3876 instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
3877     template: 'One2Many.viewmanager',
3878     init: function(parent, dataset, views, flags) {
3879         this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
3880         this.registry = this.registry.extend({
3881             list: 'instance.web.form.One2ManyListView',
3882             form: 'instance.web.form.One2ManyFormView',
3883             kanban: 'instance.web.form.One2ManyKanbanView',
3884         });
3885         this.__ignore_blur = false;
3886     },
3887     switch_mode: function(mode, unused) {
3888         if (mode !== 'form') {
3889             return this._super(mode, unused);
3890         }
3891         var self = this;
3892         var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
3893         var pop = new instance.web.form.FormOpenPopup(this);
3894         pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
3895             title: _t("Open: ") + self.o2m.string,
3896             create_function: function(data, options) {
3897                 return self.o2m.dataset.create(data, options).done(function(r) {
3898                     self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3899                     self.o2m.dataset.trigger("dataset_changed", r);
3900                 });
3901             },
3902             write_function: function(id, data, options) {
3903                 return self.o2m.dataset.write(id, data, {}).done(function() {
3904                     self.o2m.reload_current_view();
3905                 });
3906             },
3907             alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3908             parent_view: self.o2m.view,
3909             child_name: self.o2m.name,
3910             read_function: function() {
3911                 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3912             },
3913             form_view_options: {'not_interactible_on_create':true},
3914             readonly: self.o2m.get("effective_readonly")
3915         });
3916         pop.on("elements_selected", self, function() {
3917             self.o2m.reload_current_view();
3918         });
3919     },
3920 });
3921
3922 instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
3923     get_context: function() {
3924         this.context = this.o2m.build_context();
3925         return this.context;
3926     }
3927 });
3928
3929 instance.web.form.One2ManyListView = instance.web.ListView.extend({
3930     _template: 'One2Many.listview',
3931     init: function (parent, dataset, view_id, options) {
3932         this._super(parent, dataset, view_id, _.extend(options || {}, {
3933             GroupsType: instance.web.form.One2ManyGroups,
3934             ListType: instance.web.form.One2ManyList
3935         }));
3936         this.on('edit:before', this, this.proxy('_before_edit'));
3937         this.on('edit:after', this, this.proxy('_after_edit'));
3938         this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
3939
3940         this.records
3941             .bind('add', this.proxy("changed_records"))
3942             .bind('edit', this.proxy("changed_records"))
3943             .bind('remove', this.proxy("changed_records"));
3944     },
3945     start: function () {
3946         var ret = this._super();
3947         this.$el
3948             .off('mousedown.handleButtons')
3949             .on('mousedown.handleButtons', 'table button', this.proxy('_button_down'));
3950         return ret;
3951     },
3952     changed_records: function () {
3953         this.o2m.trigger_on_change();
3954     },
3955     is_valid: function () {
3956         var editor = this.editor;
3957         var form = editor.form;
3958         // If no edition is pending, the listview can not be invalid (?)
3959         if (!editor.record) {
3960             return true
3961         }
3962         // If the form has not been modified, the view can only be valid
3963         // NB: is_dirty will also be set on defaults/onchanges/whatever?
3964         // oe_form_dirty seems to only be set on actual user actions
3965         if (!form.$el.is('.oe_form_dirty')) {
3966             return true;
3967         }
3968         this.o2m._dirty_flag = true;
3969
3970         // Otherwise validate internal form
3971         return _(form.fields).chain()
3972             .invoke(function () {
3973                 this._check_css_flags();
3974                 return this.is_valid();
3975             })
3976             .all(_.identity)
3977             .value();
3978     },
3979     do_add_record: function () {
3980         if (this.editable()) {
3981             this._super.apply(this, arguments);
3982         } else {
3983             var self = this;
3984             var pop = new instance.web.form.SelectCreatePopup(this);
3985             pop.select_element(
3986                 self.o2m.field.relation,
3987                 {
3988                     title: _t("Create: ") + self.o2m.string,
3989                     initial_view: "form",
3990                     alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
3991                     create_function: function(data, options) {
3992                         return self.o2m.dataset.create(data, options).done(function(r) {
3993                             self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
3994                             self.o2m.dataset.trigger("dataset_changed", r);
3995                         });
3996                     },
3997                     read_function: function() {
3998                         return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
3999                     },
4000                     parent_view: self.o2m.view,
4001                     child_name: self.o2m.name,
4002                     form_view_options: {'not_interactible_on_create':true}
4003                 },
4004                 self.o2m.build_domain(),
4005                 self.o2m.build_context()
4006             );
4007             pop.on("elements_selected", self, function() {
4008                 self.o2m.reload_current_view();
4009             });
4010         }
4011     },
4012     do_activate_record: function(index, id) {
4013         var self = this;
4014         var pop = new instance.web.form.FormOpenPopup(self);
4015         pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
4016             title: _t("Open: ") + self.o2m.string,
4017             write_function: function(id, data) {
4018                 return self.o2m.dataset.write(id, data, {}).done(function() {
4019                     self.o2m.reload_current_view();
4020                 });
4021             },
4022             alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
4023             parent_view: self.o2m.view,
4024             child_name: self.o2m.name,
4025             read_function: function() {
4026                 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
4027             },
4028             form_view_options: {'not_interactible_on_create':true},
4029             readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
4030         });
4031     },
4032     do_button_action: function (name, id, callback) {
4033         if (!_.isNumber(id)) {
4034             instance.webclient.notification.warn(
4035                 _t("Action Button"),
4036                 _t("The o2m record must be saved before an action can be used"));
4037             return;
4038         }
4039         var parent_form = this.o2m.view;
4040         var self = this;
4041         this.ensure_saved().then(function () {
4042             if (parent_form)
4043                 return parent_form.save();
4044             else
4045                 return $.when();
4046         }).done(function () {
4047             if (!self.o2m.options.reload_on_button) {
4048                 self.handle_button(name, id, callback);
4049             }else {
4050                 self.handle_button(name, id, function(){
4051                     self.o2m.view.reload();
4052                 });
4053             }
4054         });
4055     },
4056
4057     _before_edit: function () {
4058         this.__ignore_blur = false;
4059         this.editor.form.on('blurred', this, this._on_form_blur);
4060     },
4061     _after_edit: function () {
4062         // The form's blur thing may be jiggered during the edition setup,
4063         // potentially leading to the o2m instasaving the row. Cancel any
4064         // blurring triggered the edition startup here
4065         this.editor.form.widgetFocused();
4066     },
4067     _before_unedit: function () {
4068         this.editor.form.off('blurred', this, this._on_form_blur);
4069     },
4070     _button_down: function () {
4071         // If a button is clicked (usually some sort of action button), it's
4072         // the button's responsibility to ensure the editable list is in the
4073         // correct state -> ignore form blurring
4074         this.__ignore_blur = true;
4075     },
4076     /**
4077      * Handles blurring of the nested form (saves the currently edited row),
4078      * unless the flag to ignore the event is set to ``true``
4079      *
4080      * Makes the internal form go away
4081      */
4082     _on_form_blur: function () {
4083         if (this.__ignore_blur) {
4084             this.__ignore_blur = false;
4085             return;
4086         }
4087         // FIXME: why isn't there an API for this?
4088         if (this.editor.form.$el.hasClass('oe_form_dirty')) {
4089             this.ensure_saved();
4090             return;
4091         }
4092         this.cancel_edition();
4093     },
4094     keyup_ENTER: function () {
4095         // blurring caused by hitting the [Return] key, should skip the
4096         // autosave-on-blur and let the handler for [Return] do its thing (save
4097         // the current row *anyway*, then create a new one/edit the next one)
4098         this.__ignore_blur = true;
4099         this._super.apply(this, arguments);
4100     },
4101     do_delete: function (ids) {
4102         var confirm = window.confirm;
4103         window.confirm = function () { return true; };
4104         try {
4105             return this._super(ids);
4106         } finally {
4107             window.confirm = confirm;
4108         }
4109     }
4110 });
4111 instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
4112     setup_resequence_rows: function () {
4113         if (!this.view.o2m.get('effective_readonly')) {
4114             this._super.apply(this, arguments);
4115         }
4116     }
4117 });
4118 instance.web.form.One2ManyList = instance.web.ListView.List.extend({
4119     pad_table_to: function (count) {
4120         if (!this.view.is_action_enabled('create')) {
4121             this._super(count);
4122         } else {
4123             this._super(count > 0 ? count - 1 : 0);
4124         }
4125
4126         // magical invocation of wtf does that do
4127         if (this.view.o2m.get('effective_readonly')) {
4128             return;
4129         }
4130
4131         var self = this;
4132         var columns = _(this.columns).filter(function (column) {
4133             return column.invisible !== '1';
4134         }).length;
4135         if (this.options.selectable) { columns++; }
4136         if (this.options.deletable) { columns++; }
4137
4138         if (!this.view.is_action_enabled('create')) {
4139             return;
4140         }
4141
4142         var $cell = $('<td>', {
4143             colspan: columns,
4144             'class': 'oe_form_field_one2many_list_row_add'
4145         }).append(
4146             $('<a>', {href: '#'}).text(_t("Add an item"))
4147                 .mousedown(function () {
4148                     // FIXME: needs to be an official API somehow
4149                     if (self.view.editor.is_editing()) {
4150                         self.view.__ignore_blur = true;
4151                     }
4152                 })
4153                 .click(function (e) {
4154                     e.preventDefault();
4155                     e.stopPropagation();
4156                     // FIXME: there should also be an API for that one
4157                     if (self.view.editor.form.__blur_timeout) {
4158                         clearTimeout(self.view.editor.form.__blur_timeout);
4159                         self.view.editor.form.__blur_timeout = false;
4160                     }
4161                     self.view.ensure_saved().done(function () {
4162                         self.view.do_add_record();
4163                     });
4164                 }));
4165
4166         var $padding = this.$current.find('tr:not([data-id]):first');
4167         var $newrow = $('<tr>').append($cell);
4168         if ($padding.length) {
4169             $padding.before($newrow);
4170         } else {
4171             this.$current.append($newrow)
4172         }
4173     }
4174 });
4175
4176 instance.web.form.One2ManyFormView = instance.web.FormView.extend({
4177     form_template: 'One2Many.formview',
4178     load_form: function(data) {
4179         this._super(data);
4180         var self = this;
4181         this.$buttons.find('button.oe_form_button_create').click(function() {
4182             self.save().done(self.on_button_new);
4183         });
4184     },
4185     do_notify_change: function() {
4186         if (this.dataset.parent_view) {
4187             this.dataset.parent_view.do_notify_change();
4188         } else {
4189             this._super.apply(this, arguments);
4190         }
4191     }
4192 });
4193
4194 var lazy_build_o2m_kanban_view = function() {
4195     if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
4196         return;
4197     instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4198     });
4199 };
4200
4201 instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
4202     template: "FieldMany2ManyTags",
4203     init: function() {
4204         this._super.apply(this, arguments);
4205         instance.web.form.CompletionFieldMixin.init.call(this);
4206         this.set({"value": []});
4207         this._display_orderer = new instance.web.DropMisordered();
4208         this._drop_shown = false;
4209     },
4210     initialize_content: function() {
4211         if (this.get("effective_readonly"))
4212             return;
4213         var self = this;
4214         var ignore_blur = false;
4215         self.$text = this.$("textarea");
4216         self.$text.textext({
4217             plugins : 'tags arrow autocomplete',
4218             autocomplete: {
4219                 render: function(suggestion) {
4220                     return $('<span class="text-label"/>').
4221                              data('index', suggestion['index']).html(suggestion['label']);
4222                 }
4223             },
4224             ext: {
4225                 autocomplete: {
4226                     selectFromDropdown: function() {
4227                         this.trigger('hideDropdown');
4228                         var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4229                         var data = self.search_result[index];
4230                         if (data.id) {
4231                             self.add_id(data.id);
4232                         } else {
4233                             ignore_blur = true;
4234                             data.action();
4235                         }
4236                         this.trigger('setSuggestions', {result : []});
4237                     },
4238                 },
4239                 tags: {
4240                     isTagAllowed: function(tag) {
4241                         return !!tag.name;
4242
4243                     },
4244                     removeTag: function(tag) {
4245                         var id = tag.data("id");
4246                         self.set({"value": _.without(self.get("value"), id)});
4247                     },
4248                     renderTag: function(stuff) {
4249                         return $.fn.textext.TextExtTags.prototype.renderTag.
4250                             call(this, stuff).data("id", stuff.id);
4251                     },
4252                 },
4253                 itemManager: {
4254                     itemToString: function(item) {
4255                         return item.name;
4256                     },
4257                 },
4258                 core: {
4259                     onSetInputData: function(e, data) {
4260                         if (data == '') {
4261                             this._plugins.autocomplete._suggestions = null;
4262                         }
4263                         this.input().val(data);
4264                     },
4265                 },
4266             },
4267         }).bind('getSuggestions', function(e, data) {
4268             var _this = this;
4269             var str = !!data ? data.query || '' : '';
4270             self.get_search_result(str).done(function(result) {
4271                 self.search_result = result;
4272                 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4273                     return _.extend(el, {index:i});
4274                 })});
4275             });
4276         }).bind('hideDropdown', function() {
4277             self._drop_shown = false;
4278         }).bind('showDropdown', function() {
4279             self._drop_shown = true;
4280         });
4281         self.tags = self.$text.textext()[0].tags();
4282         self.$text
4283             .focusin(function () {
4284                 self.trigger('focused');
4285                 ignore_blur = false;
4286             })
4287             .focusout(function() {
4288                 self.$text.trigger("setInputData", "");
4289                 if (!ignore_blur) {
4290                     self.trigger('blurred');
4291                 }
4292             }).keydown(function(e) {
4293                 if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
4294                     self.$text.textext()[0].autocomplete().selectFromDropdown();
4295                 }
4296             });
4297     },
4298     set_value: function(value_) {
4299         value_ = value_ || [];
4300         if (value_.length >= 1 && value_[0] instanceof Array) {
4301             value_ = value_[0][2];
4302         }
4303         this._super(value_);
4304     },
4305     is_false: function() {
4306         return _(this.get("value")).isEmpty();
4307     },
4308     get_value: function() {
4309         var tmp = [commands.replace_with(this.get("value"))];
4310         return tmp;
4311     },
4312     get_search_blacklist: function() {
4313         return this.get("value");
4314     },
4315     render_value: function() {
4316         var self = this;
4317         var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
4318         var values = self.get("value");
4319         var handle_names = function(data) {
4320             if (self.isDestroyed())
4321                 return;
4322             var indexed = {};
4323             _.each(data, function(el) {
4324                 indexed[el[0]] = el;
4325             });
4326             data = _.map(values, function(el) { return indexed[el]; });
4327             if (! self.get("effective_readonly")) {
4328                 self.tags.containerElement().children().remove();
4329                 self.$('textarea').css("padding-left", "3px");
4330                 self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
4331             } else {
4332                 self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
4333             }
4334         };
4335         if (! values || values.length > 0) {
4336             this._display_orderer.add(dataset.name_get(values)).done(handle_names);
4337         } else {
4338             handle_names([]);
4339         }
4340     },
4341     add_id: function(id) {
4342         this.set({'value': _.uniq(this.get('value').concat([id]))});
4343     },
4344     focus: function () {
4345         var input = this.$text && this.$text[0];
4346         return input ? input.focus() : false;
4347     },
4348     set_dimensions: function (height, width) {
4349         this._super(height, width);        
4350         this.$("textarea").css({
4351             width: width,
4352             minHeight: height
4353         });
4354     },
4355 });
4356
4357 /**
4358     widget options:
4359     - reload_on_button: Reload the whole form view if click on a button in a list view.
4360         If you see this options, do not use it, it's basically a dirty hack to make one
4361         precise o2m to behave the way we want.
4362 */
4363 instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4364     multi_selection: false,
4365     disable_utility_classes: true,
4366     init: function(field_manager, node) {
4367         this._super(field_manager, node);
4368         this.is_loaded = $.Deferred();
4369         this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4370         this.dataset.m2m = this;
4371         var self = this;
4372         this.dataset.on('unlink', self, function(ids) {
4373             self.dataset_changed();
4374         });
4375         this.set_value([]);
4376         this.list_dm = new instance.web.DropMisordered();
4377         this.render_value_dm = new instance.web.DropMisordered();
4378     },
4379     initialize_content: function() {
4380         var self = this;
4381
4382         this.$el.addClass('oe_form_field oe_form_field_many2many');
4383
4384         this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
4385                     'addable': this.get("effective_readonly") ? null : _t("Add"),
4386                     'deletable': this.get("effective_readonly") ? false : true,
4387                     'selectable': this.multi_selection,
4388                     'sortable': false,
4389                     'reorderable': false,
4390                     'import_enabled': false,
4391             });
4392         var embedded = (this.field.views || {}).tree;
4393         if (embedded) {
4394             this.list_view.set_embedded_view(embedded);
4395         }
4396         this.list_view.m2m_field = this;
4397         var loaded = $.Deferred();
4398         this.list_view.on("list_view_loaded", this, function() {
4399             loaded.resolve();
4400         });
4401         this.list_view.appendTo(this.$el);
4402
4403         var old_def = self.is_loaded;
4404         self.is_loaded = $.Deferred().done(function() {
4405             old_def.resolve();
4406         });
4407         this.list_dm.add(loaded).then(function() {
4408             self.is_loaded.resolve();
4409         });
4410     },
4411     destroy_content: function() {
4412         this.list_view.destroy();
4413         this.list_view = undefined;
4414     },
4415     set_value: function(value_) {
4416         value_ = value_ || [];
4417         if (value_.length >= 1 && value_[0] instanceof Array) {
4418             value_ = value_[0][2];
4419         }
4420         this._super(value_);
4421     },
4422     get_value: function() {
4423         return [commands.replace_with(this.get('value'))];
4424     },
4425     is_false: function () {
4426         return _(this.get("value")).isEmpty();
4427     },
4428     render_value: function() {
4429         var self = this;
4430         this.dataset.set_ids(this.get("value"));
4431         this.render_value_dm.add(this.is_loaded).then(function() {
4432             return self.list_view.reload_content();
4433         });
4434     },
4435     dataset_changed: function() {
4436         this.internal_set_value(this.dataset.ids);
4437     },
4438 });
4439
4440 instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
4441     get_context: function() {
4442         this.context = this.m2m.build_context();
4443         return this.context;
4444     }
4445 });
4446
4447 /**
4448  * @class
4449  * @extends instance.web.ListView
4450  */
4451 instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
4452     do_add_record: function () {
4453         var pop = new instance.web.form.SelectCreatePopup(this);
4454         pop.select_element(
4455             this.model,
4456             {
4457                 title: _t("Add: ") + this.m2m_field.string
4458             },
4459             new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
4460             this.m2m_field.build_context()
4461         );
4462         var self = this;
4463         pop.on("elements_selected", self, function(element_ids) {
4464             var reload = false;
4465             _(element_ids).each(function (id) {
4466                 if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
4467                     self.dataset.set_ids(self.dataset.ids.concat([id]));
4468                     self.m2m_field.dataset_changed();
4469                     reload = true;
4470                 }
4471             });
4472             if (reload) {
4473                 self.reload_content();
4474             }
4475         });
4476     },
4477     do_activate_record: function(index, id) {
4478         var self = this;
4479         var pop = new instance.web.form.FormOpenPopup(this);
4480         pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
4481             title: _t("Open: ") + this.m2m_field.string,
4482             readonly: this.getParent().get("effective_readonly")
4483         });
4484         pop.on('write_completed', self, self.reload_content);
4485     },
4486     do_button_action: function(name, id, callback) {
4487         var self = this;
4488         var _sup = _.bind(this._super, this);
4489         if (! this.m2m_field.options.reload_on_button) {
4490             return _sup(name, id, callback);
4491         } else {
4492             return this.m2m_field.view.save().then(function() {
4493                 return _sup(name, id, function() {
4494                     self.m2m_field.view.reload();
4495                 });
4496             });
4497         }
4498      },
4499     is_action_enabled: function () { return true; },
4500 });
4501
4502 instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
4503     disable_utility_classes: true,
4504     init: function(field_manager, node) {
4505         this._super(field_manager, node);
4506         instance.web.form.CompletionFieldMixin.init.call(this);
4507         m2m_kanban_lazy_init();
4508         this.is_loaded = $.Deferred();
4509         this.initial_is_loaded = this.is_loaded;
4510
4511         var self = this;
4512         this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
4513         this.dataset.m2m = this;
4514         this.dataset.on('unlink', self, function(ids) {
4515             self.dataset_changed();
4516         });
4517     },
4518     start: function() {
4519         this._super.apply(this, arguments);
4520
4521         var self = this;
4522
4523         self.load_view();
4524         self.on("change:effective_readonly", self, function() {
4525             self.is_loaded = self.is_loaded.then(function() {
4526                 self.kanban_view.destroy();
4527                 return $.when(self.load_view()).done(function() {
4528                     self.render_value();
4529                 });
4530             });
4531         });
4532     },
4533     set_value: function(value_) {
4534         value_ = value_ || [];
4535         if (value_.length >= 1 && value_[0] instanceof Array) {
4536             value_ = value_[0][2];
4537         }
4538         this._super(value_);
4539     },
4540     get_value: function() {
4541         return [commands.replace_with(this.get('value'))];
4542     },
4543     load_view: function() {
4544         var self = this;
4545         this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
4546                     'create_text': _t("Add"),
4547                     'creatable': self.get("effective_readonly") ? false : true,
4548                     'quick_creatable': self.get("effective_readonly") ? false : true,
4549                     'read_only_mode': self.get("effective_readonly") ? true : false,
4550                     'confirm_on_delete': false,
4551             });
4552         var embedded = (this.field.views || {}).kanban;
4553         if (embedded) {
4554             this.kanban_view.set_embedded_view(embedded);
4555         }
4556         this.kanban_view.m2m = this;
4557         var loaded = $.Deferred();
4558         this.kanban_view.on("kanban_view_loaded",self,function() {
4559             self.initial_is_loaded.resolve();
4560             loaded.resolve();
4561         });
4562         this.kanban_view.on('switch_mode', this, this.open_popup);
4563         $.async_when().done(function () {
4564             self.kanban_view.appendTo(self.$el);
4565         });
4566         return loaded;
4567     },
4568     render_value: function() {
4569         var self = this;
4570         this.dataset.set_ids(this.get("value"));
4571         this.is_loaded = this.is_loaded.then(function() {
4572             return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
4573         });
4574     },
4575     dataset_changed: function() {
4576         this.set({'value': this.dataset.ids});
4577     },
4578     open_popup: function(type, unused) {
4579         if (type !== "form")
4580             return;
4581         var self = this;
4582         if (this.dataset.index === null) {
4583             var pop = new instance.web.form.SelectCreatePopup(this);
4584             pop.select_element(
4585                 this.field.relation,
4586                 {
4587                     title: _t("Add: ") + this.string
4588                 },
4589                 new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
4590                 this.build_context()
4591             );
4592             pop.on("elements_selected", self, function(element_ids) {
4593                 _.each(element_ids, function(one_id) {
4594                     if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
4595                         self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
4596                         self.dataset_changed();
4597                         self.render_value();
4598                     }
4599                 });
4600             });
4601         } else {
4602             var id = self.dataset.ids[self.dataset.index];
4603             var pop = new instance.web.form.FormOpenPopup(this);
4604             pop.show_element(self.field.relation, id, self.build_context(), {
4605                 title: _t("Open: ") + self.string,
4606                 write_function: function(id, data, options) {
4607                     return self.dataset.write(id, data, {}).done(function() {
4608                         self.render_value();
4609                     });
4610                 },
4611                 alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
4612                 parent_view: self.view,
4613                 child_name: self.name,
4614                 readonly: self.get("effective_readonly")
4615             });
4616         }
4617     },
4618     add_id: function(id) {
4619         this.quick_create.add_id(id);
4620     },
4621 });
4622
4623 function m2m_kanban_lazy_init() {
4624 if (instance.web.form.Many2ManyKanbanView)
4625     return;
4626 instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
4627     quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
4628     _is_quick_create_enabled: function() {
4629         return this._super() && ! this.group_by;
4630     },
4631 });
4632 instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
4633     template: 'Many2ManyKanban.quick_create',
4634
4635     /**
4636      * close_btn: If true, the widget will display a "Close" button able to trigger
4637      * a "close" event.
4638      */
4639     init: function(parent, dataset, context, buttons) {
4640         this._super(parent);
4641         this.m2m = this.getParent().view.m2m;
4642         this.m2m.quick_create = this;
4643         this._dataset = dataset;
4644         this._buttons = buttons || false;
4645         this._context = context || {};
4646     },
4647     start: function () {
4648         var self = this;
4649         self.$text = this.$el.find('input').css("width", "200px");
4650         self.$text.textext({
4651             plugins : 'arrow autocomplete',
4652             autocomplete: {
4653                 render: function(suggestion) {
4654                     return $('<span class="text-label"/>').
4655                              data('index', suggestion['index']).html(suggestion['label']);
4656                 }
4657             },
4658             ext: {
4659                 autocomplete: {
4660                     selectFromDropdown: function() {
4661                         $(this).trigger('hideDropdown');
4662                         var index = Number(this.selectedSuggestionElement().children().children().data('index'));
4663                         var data = self.search_result[index];
4664                         if (data.id) {
4665                             self.add_id(data.id);
4666                         } else {
4667                             data.action();
4668                         }
4669                     },
4670                 },
4671                 itemManager: {
4672                     itemToString: function(item) {
4673                         return item.name;
4674                     },
4675                 },
4676             },
4677         }).bind('getSuggestions', function(e, data) {
4678             var _this = this;
4679             var str = !!data ? data.query || '' : '';
4680             self.m2m.get_search_result(str).done(function(result) {
4681                 self.search_result = result;
4682                 $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
4683                     return _.extend(el, {index:i});
4684                 })});
4685             });
4686         });
4687         self.$text.focusout(function() {
4688             self.$text.val("");
4689         });
4690     },
4691     focus: function() {
4692         this.$text[0].focus();
4693     },
4694     add_id: function(id) {
4695         var self = this;
4696         self.$text.val("");
4697         self.trigger('added', id);
4698         this.m2m.dataset_changed();
4699     },
4700 });
4701 }
4702
4703 /**
4704  * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
4705  */
4706 instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
4707     template: "AbstractFormPopup.render",
4708     /**
4709      *  options:
4710      *  -readonly: only applicable when not in creation mode, default to false
4711      * - alternative_form_view
4712      * - view_id
4713      * - write_function
4714      * - read_function
4715      * - create_function
4716      * - parent_view
4717      * - child_name
4718      * - form_view_options
4719      */
4720     init_popup: function(model, row_id, domain, context, options) {
4721         this.row_id = row_id;
4722         this.model = model;
4723         this.domain = domain || [];
4724         this.context = context || {};
4725         this.options = options;
4726         _.defaults(this.options, {
4727         });
4728     },
4729     init_dataset: function() {
4730         var self = this;
4731         this.created_elements = [];
4732         this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
4733         this.dataset.read_function = this.options.read_function;
4734         this.dataset.create_function = function(data, options, sup) {
4735             var fct = self.options.create_function || sup;
4736             return fct.call(this, data, options).done(function(r) {
4737                 self.trigger('create_completed saved', r);
4738                 self.created_elements.push(r);
4739             });
4740         };
4741         this.dataset.write_function = function(id, data, options, sup) {
4742             var fct = self.options.write_function || sup;
4743             return fct.call(this, id, data, options).done(function(r) {
4744                 self.trigger('write_completed saved', r);
4745             });
4746         };
4747         this.dataset.parent_view = this.options.parent_view;
4748         this.dataset.child_name = this.options.child_name;
4749     },
4750     display_popup: function() {
4751         var self = this;
4752         this.renderElement();
4753         var dialog = new instance.web.Dialog(this, {
4754             min_width: '800px',
4755             dialogClass: 'oe_act_window',
4756             close: function() {
4757                 self.check_exit(true);
4758             },
4759             title: this.options.title || "",
4760         }, this.$el).open();
4761         this.$buttonpane = dialog.$buttons;
4762         this.start();
4763     },
4764     setup_form_view: function() {
4765         var self = this;
4766         if (this.row_id) {
4767             this.dataset.ids = [this.row_id];
4768             this.dataset.index = 0;
4769         } else {
4770             this.dataset.index = null;
4771         }
4772         var options = _.clone(self.options.form_view_options) || {};
4773         if (this.row_id !== null) {
4774             options.initial_mode = this.options.readonly ? "view" : "edit";
4775         }
4776         _.extend(options, {
4777             $buttons: this.$buttonpane,
4778         });
4779         this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
4780         if (this.options.alternative_form_view) {
4781             this.view_form.set_embedded_view(this.options.alternative_form_view);
4782         }
4783         this.view_form.appendTo(this.$el.find(".oe_popup_form"));
4784         this.view_form.on("form_view_loaded", self, function() {
4785             var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
4786             self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
4787                 multi_select: multi_select,
4788                 readonly: self.row_id !== null && self.options.readonly,
4789             }));
4790             var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
4791             $snbutton.click(function() {
4792                 $.when(self.view_form.save()).done(function() {
4793                     self.view_form.reload_mutex.exec(function() {
4794                         self.view_form.on_button_new();
4795                     });
4796                 });
4797             });
4798             var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
4799             $sbutton.click(function() {
4800                 $.when(self.view_form.save()).done(function() {
4801                     self.view_form.reload_mutex.exec(function() {
4802                         self.check_exit();
4803                     });
4804                 });
4805             });
4806             var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
4807             $cbutton.click(function() {
4808                 self.view_form.trigger('on_button_cancel');
4809                 self.check_exit();
4810             });
4811             self.view_form.do_show();
4812         });
4813     },
4814     select_elements: function(element_ids) {
4815         this.trigger("elements_selected", element_ids);
4816     },
4817     check_exit: function(no_destroy) {
4818         if (this.created_elements.length > 0) {
4819             this.select_elements(this.created_elements);
4820             this.created_elements = [];
4821         }
4822         this.trigger('closed');
4823         this.destroy();
4824     },
4825     destroy: function () {
4826         this.trigger('closed');
4827         if (this.$el.is(":data(dialog)")) {
4828             this.$el.dialog('close');
4829         }
4830         this._super();
4831     },
4832 });
4833
4834 /**
4835  * Class to display a popup containing a form view.
4836  */
4837 instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
4838     show_element: function(model, row_id, context, options) {
4839         this.init_popup(model, row_id, [], context,  options);
4840         _.defaults(this.options, {
4841         });
4842         this.display_popup();
4843     },
4844     start: function() {
4845         this._super();
4846         this.init_dataset();
4847         this.setup_form_view();
4848     },
4849 });
4850
4851 /**
4852  * Class to display a popup to display a list to search a row. It also allows
4853  * to switch to a form view to create a new row.
4854  */
4855 instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
4856     /**
4857      * options:
4858      * - initial_ids
4859      * - initial_view: form or search (default search)
4860      * - disable_multiple_selection
4861      * - list_view_options
4862      */
4863     select_element: function(model, options, domain, context) {
4864         this.init_popup(model, null, domain, context, options);
4865         var self = this;
4866         _.defaults(this.options, {
4867             initial_view: "search",
4868         });
4869         this.initial_ids = this.options.initial_ids;
4870         this.display_popup();
4871     },
4872     start: function() {
4873         var self = this;
4874         this.init_dataset();
4875         if (this.options.initial_view == "search") {
4876             instance.web.pyeval.eval_domains_and_contexts({
4877                 domains: [],
4878                 contexts: [this.context]
4879             }).done(function (results) {
4880                 var search_defaults = {};
4881                 _.each(results.context, function (value_, key) {
4882                     var match = /^search_default_(.*)$/.exec(key);
4883                     if (match) {
4884                         search_defaults[match[1]] = value_;
4885                     }
4886                 });
4887                 self.setup_search_view(search_defaults);
4888             });
4889         } else { // "form"
4890             this.new_object();
4891         }
4892     },
4893     setup_search_view: function(search_defaults) {
4894         var self = this;
4895         if (this.searchview) {
4896             this.searchview.destroy();
4897         }
4898         this.searchview = new instance.web.SearchView(this,
4899                 this.dataset, false,  search_defaults);
4900         this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
4901             if (self.initial_ids) {
4902                 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
4903                     contexts, groupbys);
4904                 self.initial_ids = undefined;
4905             } else {
4906                 self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
4907             }
4908         });
4909         this.searchview.on("search_view_loaded", self, function() {
4910             self.view_list = new instance.web.form.SelectCreateListView(self,
4911                     self.dataset, false,
4912                     _.extend({'deletable': false,
4913                         'selectable': !self.options.disable_multiple_selection,
4914                         'import_enabled': false,
4915                         '$buttons': self.$buttonpane,
4916                         'disable_editable_mode': true,
4917                         '$pager': self.$('.oe_popup_list_pager'),
4918                     }, self.options.list_view_options || {}));
4919             self.view_list.on('edit:before', self, function (e) {
4920                 e.cancel = true;
4921             });
4922             self.view_list.popup = self;
4923             self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
4924                 self.view_list.do_show();
4925             }).then(function() {
4926                 self.searchview.do_search();
4927             });
4928             self.view_list.on("list_view_loaded", self, function() {
4929                 self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
4930                 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
4931                 $cbutton.click(function() {
4932                     self.destroy();
4933                 });
4934                 var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
4935                 $sbutton.click(function() {
4936                     self.select_elements(self.selected_ids);
4937                     self.destroy();
4938                 });
4939                 var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
4940                 $cbutton.click(function() {
4941                     self.new_object();
4942                 });
4943             });
4944         });
4945         this.searchview.appendTo($(".oe_popup_search", self.$el));
4946     },
4947     do_search: function(domains, contexts, groupbys) {
4948         var self = this;
4949         instance.web.pyeval.eval_domains_and_contexts({
4950             domains: domains || [],
4951             contexts: contexts || [],
4952             group_by_seq: groupbys || []
4953         }).done(function (results) {
4954             self.view_list.do_search(results.domain, results.context, results.group_by);
4955         });
4956     },
4957     on_click_element: function(ids) {
4958         var self = this;
4959         this.selected_ids = ids || [];
4960         if(this.selected_ids.length > 0) {
4961             self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
4962         } else {
4963             self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
4964         }
4965     },
4966     new_object: function() {
4967         if (this.searchview) {
4968             this.searchview.hide();
4969         }
4970         if (this.view_list) {
4971             this.view_list.do_hide();
4972         }
4973         this.setup_form_view();
4974     },
4975 });
4976
4977 instance.web.form.SelectCreateListView = instance.web.ListView.extend({
4978     do_add_record: function () {
4979         this.popup.new_object();
4980     },
4981     select_record: function(index) {
4982         this.popup.select_elements([this.dataset.ids[index]]);
4983         this.popup.destroy();
4984     },
4985     do_select: function(ids, records) {
4986         this._super(ids, records);
4987         this.popup.on_click_element(ids);
4988     }
4989 });
4990
4991 instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
4992     template: 'FieldReference',
4993     init: function(field_manager, node) {
4994         this._super(field_manager, node);
4995         this.reference_ready = true;
4996     },
4997     destroy_content: function() {
4998         if (this.fm) {
4999             this.fm.destroy();
5000             this.fm = undefined;
5001         }
5002     },
5003     initialize_content: function() {
5004         var self = this;
5005         var fm = new instance.web.form.DefaultFieldManager(this);
5006         this.fm = fm;
5007         fm.extend_field_desc({
5008             "selection": {
5009                 selection: this.field_manager.get_field_desc(this.name).selection,
5010                 type: "selection",
5011             },
5012             "m2o": {
5013                 relation: null,
5014                 type: "many2one",
5015             },
5016         });
5017         this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
5018             name: 'selection',
5019             modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5020         }});
5021         this.selection.on("change:value", this, this.on_selection_changed);
5022         this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
5023         this.selection
5024             .on('focused', null, function () {self.trigger('focused')})
5025             .on('blurred', null, function () {self.trigger('blurred')});
5026
5027         this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
5028             name: 'm2o',
5029             modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
5030         }});
5031         this.m2o.on("change:value", this, this.data_changed);
5032         this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
5033         this.m2o
5034             .on('focused', null, function () {self.trigger('focused')})
5035             .on('blurred', null, function () {self.trigger('blurred')});
5036     },
5037     on_selection_changed: function() {
5038         if (this.reference_ready) {
5039             this.internal_set_value([this.selection.get_value(), false]);
5040             this.render_value();
5041         }
5042     },
5043     data_changed: function() {
5044         if (this.reference_ready) {
5045             this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
5046         }
5047     },
5048     set_value: function(val) {
5049         if (val) {
5050             val = val.split(',');
5051             val[0] = val[0] || false;
5052             val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
5053         }
5054         this._super(val || [false, false]);
5055     },
5056     get_value: function() {
5057         return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
5058     },
5059     render_value: function() {
5060         this.reference_ready = false;
5061         if (!this.get("effective_readonly")) {
5062             this.selection.set_value(this.get('value')[0]);
5063         }
5064         this.m2o.field.relation = this.get('value')[0];
5065         this.m2o.set_value(this.get('value')[1]);
5066         this.m2o.$el.toggle(!!this.get('value')[0]);
5067         this.reference_ready = true;
5068     },
5069 });
5070
5071 instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
5072     init: function(field_manager, node) {
5073         var self = this;
5074         this._super(field_manager, node);
5075         this.binary_value = false;
5076         this.useFileAPI = !!window.FileReader;
5077         this.max_upload_size = 25 * 1024 * 1024; // 25Mo
5078         if (!this.useFileAPI) {
5079             this.fileupload_id = _.uniqueId('oe_fileupload');
5080             $(window).on(this.fileupload_id, function() {
5081                 var args = [].slice.call(arguments).slice(1);
5082                 self.on_file_uploaded.apply(self, args);
5083             });
5084         }
5085     },
5086     stop: function() {
5087         if (!this.useFileAPI) {
5088             $(window).off(this.fileupload_id);
5089         }
5090         this._super.apply(this, arguments);
5091     },
5092     initialize_content: function() {
5093         this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
5094         this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
5095         this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
5096     },
5097     on_file_change: function(e) {
5098         var self = this;
5099         var file_node = e.target;
5100         if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
5101             if (this.useFileAPI) {
5102                 var file = file_node.files[0];
5103                 if (file.size > this.max_upload_size) {
5104                     var msg = _t("The selected file exceed the maximum file size of %s.");
5105                     instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
5106                     return false;
5107                 }
5108                 var filereader = new FileReader();
5109                 filereader.readAsDataURL(file);
5110                 filereader.onloadend = function(upload) {
5111                     var data = upload.target.result;
5112                     data = data.split(',')[1];
5113                     self.on_file_uploaded(file.size, file.name, file.type, data);
5114                 };
5115             } else {
5116                 this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
5117                 this.$el.find('form.oe_form_binary_form').submit();
5118             }
5119             this.$el.find('.oe_form_binary_progress').show();
5120             this.$el.find('.oe_form_binary').hide();
5121         }
5122     },
5123     on_file_uploaded: function(size, name, content_type, file_base64) {
5124         if (size === false) {
5125             this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
5126             // TODO: use openerp web crashmanager
5127             console.warn("Error while uploading file : ", name);
5128         } else {
5129             this.filename = name;
5130             this.on_file_uploaded_and_valid.apply(this, arguments);
5131         }
5132         this.$el.find('.oe_form_binary_progress').hide();
5133         this.$el.find('.oe_form_binary').show();
5134     },
5135     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5136     },
5137     on_save_as: function(ev) {
5138         var value = this.get('value');
5139         if (!value) {
5140             this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
5141             ev.stopPropagation();
5142         } else {
5143             instance.web.blockUI();
5144             var c = instance.webclient.crashmanager;
5145             this.session.get_file({
5146                 url: '/web/binary/saveas_ajax',
5147                 data: {data: JSON.stringify({
5148                     model: this.view.dataset.model,
5149                     id: (this.view.datarecord.id || ''),
5150                     field: this.name,
5151                     filename_field: (this.node.attrs.filename || ''),
5152                     data: instance.web.form.is_bin_size(value) ? null : value,
5153                     context: this.view.dataset.get_context()
5154                 })},
5155                 complete: instance.web.unblockUI,
5156                 error: c.rpc_error.bind(c)
5157             });
5158             ev.stopPropagation();
5159             return false;
5160         }
5161     },
5162     set_filename: function(value) {
5163         var filename = this.node.attrs.filename;
5164         if (filename) {
5165             var tmp = {};
5166             tmp[filename] = value;
5167             this.field_manager.set_values(tmp);
5168         }
5169     },
5170     on_clear: function() {
5171         if (this.get('value') !== false) {
5172             this.binary_value = false;
5173             this.internal_set_value(false);
5174         }
5175         return false;
5176     }
5177 });
5178
5179 instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
5180     template: 'FieldBinaryFile',
5181     initialize_content: function() {
5182         this._super();
5183         if (this.get("effective_readonly")) {
5184             var self = this;
5185             this.$el.find('a').click(function(ev) {
5186                 if (self.get('value')) {
5187                     self.on_save_as(ev);
5188                 }
5189                 return false;
5190             });
5191         }
5192     },
5193     render_value: function() {
5194         if (!this.get("effective_readonly")) {
5195             var show_value;
5196             if (this.node.attrs.filename) {
5197                 show_value = this.view.datarecord[this.node.attrs.filename] || '';
5198             } else {
5199                 show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
5200             }
5201             this.$el.find('input').eq(0).val(show_value);
5202         } else {
5203             this.$el.find('a').toggle(!!this.get('value'));
5204             if (this.get('value')) {
5205                 var show_value = _t("Download")
5206                 if (this.view)
5207                     show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
5208                 this.$el.find('a').text(show_value);
5209             }
5210         }
5211     },
5212     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5213         this.binary_value = true;
5214         this.internal_set_value(file_base64);
5215         var show_value = name + " (" + instance.web.human_size(size) + ")";
5216         this.$el.find('input').eq(0).val(show_value);
5217         this.set_filename(name);
5218     },
5219     on_clear: function() {
5220         this._super.apply(this, arguments);
5221         this.$el.find('input').eq(0).val('');
5222         this.set_filename('');
5223     }
5224 });
5225
5226 instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
5227     template: 'FieldBinaryImage',
5228     placeholder: "/web/static/src/img/placeholder.png",
5229     render_value: function() {
5230         var self = this;
5231         var url;
5232         if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
5233             url = 'data:image/png;base64,' + this.get('value');
5234         } else if (this.get('value')) {
5235             var id = JSON.stringify(this.view.datarecord.id || null);
5236             var field = this.name;
5237             if (this.options.preview_image)
5238                 field = this.options.preview_image;
5239             url = this.session.url('/web/binary/image', {
5240                                         model: this.view.dataset.model,
5241                                         id: id,
5242                                         field: field,
5243                                         t: (new Date().getTime()),
5244             });
5245         } else {
5246             url = this.placeholder;
5247         }
5248         var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
5249         this.$el.find('> img').remove();
5250         this.$el.prepend($img);
5251         $img.load(function() {
5252             if (! self.options.size)
5253                 return;
5254             $img.css("max-width", "" + self.options.size[0] + "px");
5255             $img.css("max-height", "" + self.options.size[1] + "px");
5256             $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
5257             $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
5258         });
5259         $img.on('error', function() {
5260             $img.attr('src', self.placeholder);
5261             instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
5262         });
5263     },
5264     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
5265         this.internal_set_value(file_base64);
5266         this.binary_value = true;
5267         this.render_value();
5268         this.set_filename(name);
5269     },
5270     on_clear: function() {
5271         this._super.apply(this, arguments);
5272         this.render_value();
5273         this.set_filename('');
5274     }
5275 });
5276
5277 /**
5278  * Widget for (many2many field) to upload one or more file in same time and display in list.
5279  * The user can delete his files.
5280  * Options on attribute ; "blockui" {Boolean} block the UI or not
5281  * during the file is uploading
5282  */
5283 instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
5284     template: "FieldBinaryFileUploader",
5285     init: function(field_manager, node) {
5286         this._super(field_manager, node);
5287         this.field_manager = field_manager;
5288         this.node = node;
5289         if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
5290             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);
5291         }
5292         this.data = {};
5293         this.set_value([]);
5294         this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
5295         this.fileupload_id = _.uniqueId('oe_fileupload_temp');
5296         $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
5297     },
5298     start: function() {
5299         this._super(this);
5300         this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
5301     },
5302     set_value: function(value_) {
5303         value_ = value_ || [];
5304         if (value_.length >= 1 && value_[0] instanceof Array) {
5305             value_ = value_[0][2];
5306         }
5307         this._super(value_);
5308     },
5309     get_value: function() {
5310         var tmp = [commands.replace_with(this.get("value"))];
5311         return tmp;
5312     },
5313     get_file_url: function (attachment) {
5314         return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
5315     },
5316     read_name_values : function () {
5317         var self = this;
5318         // don't reset know values
5319         var _value = _.filter(this.get('value'), function (id) { return typeof self.data[id] == 'undefined'; } );
5320         // send request for get_name
5321         if (_value.length) {
5322             return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).done(function (datas) {
5323                 _.each(datas, function (data) {
5324                     data.no_unlink = true;
5325                     data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
5326                     self.data[data.id] = data;
5327                 });
5328             });
5329         } else {
5330             return $.when();
5331         }
5332     },
5333     render_value: function () {
5334         var self = this;
5335         this.read_name_values().then(function () {
5336
5337             var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
5338             render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
5339             self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
5340
5341             // reinit input type file
5342             var $input = self.$('input.oe_form_binary_file');
5343             $input.after($input.clone(true)).remove();
5344             self.$(".oe_fileupload").show();
5345
5346         });
5347     },
5348     on_file_change: function (event) {
5349         event.stopPropagation();
5350         var self = this;
5351         var $target = $(event.target);
5352         if ($target.val() !== '') {
5353             var filename = $target.val().replace(/.*[\\\/]/,'');
5354             // don't uplode more of one file in same time
5355             if (self.data[0] && self.data[0].upload ) {
5356                 return false;
5357             }
5358             for (var id in this.get('value')) {
5359                 // if the files exits, delete the file before upload (if it's a new file)
5360                 if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) {
5361                     self.ds_file.unlink([id]);
5362                 }
5363             }
5364
5365             // block UI or not
5366             if(this.node.attrs.blockui>0) {
5367                 instance.web.blockUI();
5368             }
5369
5370             // TODO : unactivate send on wizard and form
5371
5372             // submit file
5373             this.$('form.oe_form_binary_form').submit();
5374             this.$(".oe_fileupload").hide();
5375             // add file on data result
5376             this.data[0] = {
5377                 'id': 0,
5378                 'name': filename,
5379                 'filename': filename,
5380                 'url': '',
5381                 'upload': true
5382             };
5383         }
5384     },
5385     on_file_loaded: function (event, result) {
5386         var files = this.get('value');
5387
5388         // unblock UI
5389         if(this.node.attrs.blockui>0) {
5390             instance.web.unblockUI();
5391         }
5392
5393         if (result.error || !result.id ) {
5394             this.do_warn( _t('Uploading Error'), result.error);
5395             delete this.data[0];
5396         } else {
5397             if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) {
5398                 delete this.data[0];
5399                 this.data[result.id] = {
5400                     'id': result.id,
5401                     'name': result.name,
5402                     'filename': result.filename,
5403                     'url': this.get_file_url(result)
5404                 };
5405             } else {
5406                 this.data[result.id] = {
5407                     'id': result.id,
5408                     'name': result.name,
5409                     'filename': result.filename,
5410                     'url': this.get_file_url(result)
5411                 };
5412             }
5413             var values = _.clone(this.get('value'));
5414             values.push(result.id);
5415             this.set({'value': values});
5416         }
5417         this.render_value()
5418     },
5419     on_file_delete: function (event) {
5420         event.stopPropagation();
5421         var file_id=$(event.target).data("id");
5422         if (file_id) {
5423             var files = _.filter(this.get('value'), function (id) {return id != file_id;});
5424             if(!this.data[file_id].no_unlink) {
5425                 this.ds_file.unlink([file_id]);
5426             }
5427             this.set({'value': files});
5428         }
5429     },
5430 });
5431
5432 instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
5433     template: "FieldStatus",
5434     init: function(field_manager, node) {
5435         this._super(field_manager, node);
5436         this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
5437         this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
5438         this.set({value: false});
5439         this.selection = [];
5440         this.set("selection", []);
5441         this.selection_dm = new instance.web.DropMisordered();
5442     },
5443     start: function() {
5444         this.field_manager.on("view_content_has_changed", this, this.calc_domain);
5445         this.calc_domain();
5446         this.on("change:value", this, this.get_selection);
5447         this.on("change:evaluated_selection_domain", this, this.get_selection);
5448         this.on("change:selection", this, function() {
5449             this.selection = this.get("selection");
5450             this.render_value();
5451         });
5452         this.get_selection();
5453         if (this.options.clickable) {
5454             this.$el.on('click','li',this.on_click_stage);
5455         }
5456         if (this.$el.parent().is('header')) {
5457             this.$el.after('<div class="oe_clear"/>');
5458         }
5459         this._super();
5460     },
5461     set_value: function(value_) {
5462         if (value_ instanceof Array) {
5463             value_ = value_[0];
5464         }
5465         this._super(value_);
5466     },
5467     render_value: function() {
5468         var self = this;
5469         var content = QWeb.render("FieldStatus.content", {widget: self});
5470         self.$el.html(content);
5471     },
5472     calc_domain: function() {
5473         var d = instance.web.pyeval.eval('domain', this.build_domain());
5474         var domain = []; //if there is no domain defined, fetch all the records
5475         
5476         if (d.length) {
5477             domain = ['|',['id', '=', this.get('value')]].concat(d);
5478         }
5479         
5480         if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
5481             this.set("evaluated_selection_domain", domain);
5482         }
5483     },
5484     /** Get the selection and render it
5485      *  selection: [[identifier, value_to_display], ...]
5486      *  For selection fields: this is directly given by this.field.selection
5487      *  For many2one fields:  perform a search on the relation of the many2one field
5488      */
5489     get_selection: function() {
5490         var self = this;
5491         var selection = [];
5492
5493         var calculation = _.bind(function() {
5494             if (this.field.type == "many2one") {
5495                 var domain = [];
5496                 var ds = new instance.web.DataSetSearch(this, this.field.relation,
5497                     self.build_context(), this.get("evaluated_selection_domain"));
5498                 return ds.read_slice(['name'], {}).then(function (records) {
5499                     for(var i = 0; i < records.length; i++) {
5500                         selection.push([records[i].id, records[i].name]);
5501                     }
5502                 });
5503             } else {
5504                 // For field type selection filter values according to
5505                 // statusbar_visible attribute of the field. For example:
5506                 // statusbar_visible="draft,open".
5507                 var select = this.field.selection;
5508                 for(var i=0; i < select.length; i++) {
5509                     var key = select[i][0];
5510                     if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
5511                         selection.push(select[i]);
5512                     }
5513                 }
5514                 return $.when();
5515             }
5516         }, this);
5517         this.selection_dm.add(calculation()).then(function () {
5518             if (! _.isEqual(selection, self.get("selection"))) {
5519                 self.set("selection", selection);
5520             }
5521         });
5522     },
5523     on_click_stage: function (ev) {
5524         var self = this;
5525         var $li = $(ev.currentTarget);
5526         var val = parseInt($li.data("id"));
5527         if (val != self.get('value')) {
5528             this.view.recursive_save().done(function() {
5529                 var change = {};
5530                 change[self.name] = val;
5531                 self.view.dataset.write(self.view.datarecord.id, change).done(function() {
5532                     self.view.reload();
5533                 });
5534             });
5535         }
5536     },
5537 });
5538
5539 instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
5540     template: "FieldMonetary",
5541     widget_class: 'oe_form_field_float oe_form_field_monetary',
5542     init: function() {
5543         this._super.apply(this, arguments);
5544         this.set({"currency": false});
5545         if (this.options.currency_field) {
5546             this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
5547                 this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
5548             });
5549         }
5550         this.on("change:currency", this, this.get_currency_info);
5551         this.get_currency_info();
5552         this.ci_dm = new instance.web.DropMisordered();
5553     },
5554     start: function() {
5555         var tmp = this._super();
5556         this.on("change:currency_info", this, this.reinitialize);
5557         return tmp;
5558     },
5559     get_currency_info: function() {
5560         var self = this;
5561         if (this.get("currency") === false) {
5562             this.set({"currency_info": null});
5563             return;
5564         }
5565         return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
5566             .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
5567             self.set({"currency_info": res});
5568         });
5569     },
5570     parse_value: function(val, def) {
5571         return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5572     },
5573     format_value: function(val, def) {
5574         return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
5575     },
5576 });
5577
5578 /**
5579  * Registry of form fields, called by :js:`instance.web.FormView`.
5580  *
5581  * All referenced classes must implement FieldInterface. Those represent the classes whose instances
5582  * will substitute to the <field> tags as defined in OpenERP's views.
5583  */
5584 instance.web.form.widgets = new instance.web.Registry({
5585     'char' : 'instance.web.form.FieldChar',
5586     'id' : 'instance.web.form.FieldID',
5587     'email' : 'instance.web.form.FieldEmail',
5588     'url' : 'instance.web.form.FieldUrl',
5589     'text' : 'instance.web.form.FieldText',
5590     'html' : 'instance.web.form.FieldTextHtml',
5591     'date' : 'instance.web.form.FieldDate',
5592     'datetime' : 'instance.web.form.FieldDatetime',
5593     'selection' : 'instance.web.form.FieldSelection',
5594     'radio' : 'instance.web.form.FieldRadio',
5595     'many2one' : 'instance.web.form.FieldMany2One',
5596     'many2onebutton' : 'instance.web.form.Many2OneButton',
5597     'many2many' : 'instance.web.form.FieldMany2Many',
5598     'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
5599     'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
5600     'one2many' : 'instance.web.form.FieldOne2Many',
5601     'one2many_list' : 'instance.web.form.FieldOne2Many',
5602     'reference' : 'instance.web.form.FieldReference',
5603     'boolean' : 'instance.web.form.FieldBoolean',
5604     'float' : 'instance.web.form.FieldFloat',
5605     'integer': 'instance.web.form.FieldFloat',
5606     'float_time': 'instance.web.form.FieldFloat',
5607     'progressbar': 'instance.web.form.FieldProgressBar',
5608     'image': 'instance.web.form.FieldBinaryImage',
5609     'binary': 'instance.web.form.FieldBinaryFile',
5610     'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
5611     'statusbar': 'instance.web.form.FieldStatus',
5612     'monetary': 'instance.web.form.FieldMonetary',
5613 });
5614
5615 /**
5616  * Registry of widgets usable in the form view that can substitute to any possible
5617  * tags defined in OpenERP's form views.
5618  *
5619  * Every referenced class should extend FormWidget.
5620  */
5621 instance.web.form.tags = new instance.web.Registry({
5622     'button' : 'instance.web.form.WidgetButton',
5623 });
5624
5625 instance.web.form.custom_widgets = new instance.web.Registry({
5626 });
5627
5628 };
5629
5630 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: