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