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