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