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