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