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