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