[fix] potential problem with m2o
[odoo/odoo.git] / addons / web / static / src / js / view_form.js
1 openerp.web.form = function (openerp) {
2
3 var _t = openerp.web._t;
4 var QWeb = openerp.web.qweb;
5
6 openerp.web.views.add('form', 'openerp.web.FormView');
7 openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# */{
8     /**
9      * Indicates that this view is not searchable, and thus that no search
10      * view should be displayed (if there is one active).
11      */
12     searchable: false,
13     form_template: "FormView",
14     identifier_prefix: 'formview-',
15     /**
16      * @constructs openerp.web.FormView
17      * @extends openerp.web.View
18      *
19      * @param {openerp.web.Session} session the current openerp session
20      * @param {openerp.web.DataSet} dataset the dataset this view will work with
21      * @param {String} view_id the identifier of the OpenERP view object
22      *
23      * @property {openerp.web.Registry} registry=openerp.web.form.widgets widgets registry for this form view instance
24      */
25     init: function(parent, dataset, view_id, options) {
26         this._super(parent);
27         this.set_default_options(options);
28         this.dataset = dataset;
29         this.model = dataset.model;
30         this.view_id = view_id || false;
31         this.fields_view = {};
32         this.widgets = {};
33         this.widgets_counter = 0;
34         this.fields = {};
35         this.datarecord = {};
36         this.show_invalid = true;
37         this.dirty_for_user = false;
38         this.default_focus_field = null;
39         this.default_focus_button = null;
40         this.registry = openerp.web.form.widgets;
41         this.has_been_loaded = $.Deferred();
42         this.$form_header = null;
43         this.translatable_fields = [];
44         _.defaults(this.options, {"always_show_new_button": true,
45             "not_interactible_on_create": false});
46         this.mutating_lock = $.Deferred();
47         this.initial_mutating_lock = this.mutating_lock;
48         this.on_change_lock = $.Deferred().resolve();
49         this.reload_lock = $.Deferred().resolve();
50     },
51     start: function() {
52         this._super();
53         return this.init_view();
54     },
55     init_view: function() {
56         if (this.embedded_view) {
57             var def = $.Deferred().then(this.on_loaded);
58             var self = this;
59             setTimeout(function() {def.resolve(self.embedded_view);}, 0);
60             return def.promise();
61         } else {
62             var context = new openerp.web.CompoundContext(this.dataset.get_context());
63             return this.rpc("/web/view/load", {
64                 "model": this.model,
65                 "view_id": this.view_id,
66                 "view_type": "form",
67                 toolbar: this.options.sidebar,
68                 context: context
69                 }, this.on_loaded);
70         }
71     },
72     stop: function() {
73         if (this.sidebar) {
74             this.sidebar.attachments.stop();
75             this.sidebar.stop();
76         }
77         _.each(this.widgets, function(w) {
78             w.stop();
79         });
80         this._super();
81     },
82     reposition: function ($e) {
83         this.$element = $e;
84         this.on_loaded();
85     },
86     on_loaded: function(data) {
87         var self = this;
88         if (data) {
89             this.fields_view = data;
90             var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
91
92             this.rendered = QWeb.render(this.form_template, { 'frame': frame, 'widget': this });
93         }
94         this.$element.html(this.rendered);
95         _.each(this.widgets, function(w) {
96             w.start();
97         });
98         this.$form_header = this.$element.find('.oe_form_header:first');
99         this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() {
100             var action = $(this).data('pager-action');
101             self.on_pager_action(action);
102         });
103
104         this.$form_header.find('button.oe_form_button_save').click(this.do_save);
105         this.$form_header.find('button.oe_form_button_cancel').click(this.do_cancel);
106         this.$form_header.find('button.oe_form_button_new').click(this.on_button_new);
107         this.$form_header.find('button.oe_form_button_duplicate').click(this.on_button_duplicate);
108         this.$form_header.find('button.oe_form_button_toggle').click(this.on_toggle_readonly);
109
110         if (this.options.sidebar && this.options.sidebar_id) {
111             this.sidebar = new openerp.web.Sidebar(this, this.options.sidebar_id);
112             this.sidebar.start();
113             this.sidebar.do_unfold();
114             this.sidebar.attachments = new openerp.web.form.SidebarAttachments(this.sidebar, this);
115             this.sidebar.add_toolbar(this.fields_view.toolbar);
116             this.set_common_sidebar_sections(this.sidebar);
117         }
118         this.has_been_loaded.resolve();
119     },
120     on_toggle_readonly: function() {
121         var self = this;
122         self.translatable_fields = [];
123         self.widgets = {};
124         self.fields = {};
125         self.$form_header.find('button').unbind('click');
126         self.registry = self.registry === openerp.web.form.widgets
127                 ? openerp.web.form.readonly
128                 : openerp.web.form.widgets;
129         self.on_loaded(self.fields_view);
130         self.reload();
131     },
132     do_show: function () {
133         var promise;
134         if (this.dataset.index === null) {
135             // null index means we should start a new record
136             promise = this.on_button_new();
137         } else {
138             promise = this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
139         }
140         this.$element.show();
141         if (this.sidebar) {
142             this.sidebar.$element.show();
143         }
144         return promise;
145     },
146     do_hide: function () {
147         this.$element.hide();
148         if (this.sidebar) {
149             this.sidebar.$element.hide();
150         }
151     },
152     on_record_loaded: function(record) {
153         if (!record) {
154             throw("Form: No record received");
155         }
156         if (!record.id) {
157             this.$form_header.find('.oe_form_on_create').show();
158             this.$form_header.find('.oe_form_on_update').hide();
159             if (!this.options["always_show_new_button"]) {
160                 this.$form_header.find('button.oe_form_button_new').hide();
161             }
162         } else {
163             this.$form_header.find('.oe_form_on_create').hide();
164             this.$form_header.find('.oe_form_on_update').show();
165             this.$form_header.find('button.oe_form_button_new').show();
166         }
167         this.dirty_for_user = false;
168         this.datarecord = record;
169         for (var f in this.fields) {
170             var field = this.fields[f];
171             field.dirty = false;
172             field.set_value(this.datarecord[f] || false);
173             field.validate();
174         }
175         if (!record.id) {
176             // New record: Second pass in order to trigger the onchanges
177             this.show_invalid = false;
178             for (var f in record) {
179                 var field = this.fields[f];
180                 if (field) {
181                     field.dirty = true;
182                     this.do_onchange(field);
183                 }
184             }
185         }
186         this.on_form_changed();
187         this.initial_mutating_lock.resolve();
188         this.show_invalid = true;
189         this.do_update_pager(record.id == null);
190         if (this.sidebar) {
191             this.sidebar.attachments.do_update();
192             this.sidebar.$element.find('.oe_sidebar_translate').toggleClass('oe_hide', !record.id);
193         }
194         if (this.default_focus_field && !this.embedded_view) {
195             this.default_focus_field.focus();
196         }
197     },
198     on_form_changed: function() {
199         for (var w in this.widgets) {
200             w = this.widgets[w];
201             w.process_modifiers();
202             w.update_dom();
203         }
204     },
205     on_pager_action: function(action) {
206         if (this.can_be_discarded()) {
207             switch (action) {
208                 case 'first':
209                     this.dataset.index = 0;
210                     break;
211                 case 'previous':
212                     this.dataset.previous();
213                     break;
214                 case 'next':
215                     this.dataset.next();
216                     break;
217                 case 'last':
218                     this.dataset.index = this.dataset.ids.length - 1;
219                     break;
220             }
221             this.reload();
222         }
223     },
224     do_update_pager: function(hide_index) {
225         var $pager = this.$form_header.find('div.oe_form_pager');
226         var index = hide_index ? '-' : this.dataset.index + 1;
227         $pager.find('span.oe_pager_index').html(index);
228         $pager.find('span.oe_pager_count').html(this.dataset.ids.length);
229     },
230     do_onchange: function(widget, processed) {
231         var self = this;
232         var act = function() {
233             try {
234             processed = processed || [];
235             if (widget.node.attrs.on_change) {
236                 var onchange = _.trim(widget.node.attrs.on_change);
237                 var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
238                 if (call) {
239                     var method = call[1], args = [];
240                     var context_index = null;
241                     var argument_replacement = {
242                         'False' : function() {return false;},
243                         'True' : function() {return true;},
244                         'None' : function() {return null;},
245                         'context': function(i) {
246                             context_index = i;
247                             var ctx = widget.build_context ? widget.build_context() : {};
248                             return ctx;
249                         }
250                     };
251                     var parent_fields = null;
252                     _.each(call[2].split(','), function(a, i) {
253                         var field = _.trim(a);
254                         if (field in argument_replacement) {
255                             args.push(argument_replacement[field](i));
256                             return;
257                         } else if (self.fields[field]) {
258                             var value = self.fields[field].get_on_change_value();
259                             args.push(value == null ? false : value);
260                             return;
261                         } else {
262                             var splitted = field.split('.');
263                             if (splitted.length > 1 && _.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
264                                 if (parent_fields === null) {
265                                     parent_fields = self.dataset.parent_view.get_fields_values();
266                                 }
267                                 var p_val = parent_fields[_.trim(splitted[1])];
268                                 if (p_val !== undefined) {
269                                     args.push(p_val == null ? false : p_val);
270                                     return;
271                                 }
272                             }
273                         }
274                         throw "Could not get field with name '" + field +
275                             "' for onchange '" + onchange + "'";
276                     });
277                     var ajax = {
278                         url: '/web/dataset/call',
279                         async: false
280                     };
281                     return self.rpc(ajax, {
282                         model: self.dataset.model,
283                         method: method,
284                         args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(args),
285                         context_id: context_index === null ? null : context_index + 1
286                     }).pipe(function(response) {
287                         return self.on_processed_onchange(response, processed);
288                     });
289                 } else {
290                     console.log("Wrong on_change format", on_change);
291                 }
292             }
293             } catch(e) {
294                 console.error(e);
295                 return $.Deferred().reject();
296             }
297         };
298         this.on_change_lock = this.on_change_lock.pipe(act, act);
299         return this.on_change_lock;
300     },
301     on_processed_onchange: function(response, processed) {
302         try {
303         var result = response;
304         if (result.value) {
305             for (var f in result.value) {
306                 var field = this.fields[f];
307                 // If field is not defined in the view, just ignore it
308                 if (field) {
309                     var value = result.value[f];
310                     processed.push(field.name);
311                     if (field.get_value() != value) {
312                         field.set_value(value);
313                         field.dirty = this.dirty_for_user = true;
314                         if (_.indexOf(processed, field.name) < 0) {
315                             this.do_onchange(field, processed);
316                         }
317                     }
318                 }
319             }
320             this.on_form_changed();
321         }
322         if (!_.isEmpty(result.warning)) {
323             $(QWeb.render("DialogWarning", result.warning)).dialog({
324                 modal: true,
325                 buttons: {
326                     Ok: function() {
327                         $(this).dialog("close");
328                     }
329                 }
330             });
331         }
332         if (result.domain) {
333             // TODO:
334         }
335         return $.Deferred().resolve();
336         } catch(e) {
337             console.error(e);
338             return $.Deferred().reject();
339         }
340     },
341     on_button_new: function() {
342         var self = this;
343         var def = $.Deferred();
344         $.when(this.has_been_loaded).then(function() {
345             if (self.can_be_discarded()) {
346                 var keys = _.keys(self.fields_view.fields);
347                 if (keys.length) {
348                     self.dataset.default_get(keys).then(self.on_record_loaded).then(function() {
349                         def.resolve();
350                     });
351                 } else {
352                     self.on_record_loaded({});
353                     def.resolve();
354                 }
355             }
356         });
357         return def.promise();
358     },
359     on_button_duplicate: function() {
360         var self = this;
361         var def = $.Deferred();
362         $.when(this.has_been_loaded).then(function() {
363             if (self.can_be_discarded()) {
364                 self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
365                     return self.on_created({ result : new_id });
366                 }).then(function() {
367                     def.resolve();
368                 });
369             }
370         });
371         return def.promise();
372     },
373     can_be_discarded: function() {
374         return !this.dirty_for_user || confirm(_t("Warning, the record has been modified, your changes will be discarded."));
375     },
376     /**
377      * Triggers saving the form's record. Chooses between creating a new
378      * record or saving an existing one depending on whether the record
379      * already has an id property.
380      *
381      * @param {Function} success callback on save success
382      * @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)
383      */
384     do_save: function(success, prepend_on_create) {
385         var self = this;
386         var action = function() {
387             try {
388             if (!self.initial_mutating_lock.isResolved() && !self.initial_mutating_lock.isRejected())
389                 return;
390             var form_dirty = false,
391                 form_invalid = false,
392                 values = {},
393                 first_invalid_field = null;
394             for (var f in self.fields) {
395                 f = self.fields[f];
396                 if (!f.is_valid()) {
397                     form_invalid = true;
398                     f.update_dom();
399                     if (!first_invalid_field) {
400                         first_invalid_field = f;
401                     }
402                 } else if (f.is_dirty()) {
403                     form_dirty = true;
404                     values[f.name] = f.get_value();
405                 }
406             }
407             if (form_invalid) {
408                 first_invalid_field.focus();
409                 self.on_invalid();
410                 return $.Deferred().reject();
411             } else {
412                 console.log("About to save", values);
413                 if (!self.datarecord.id) {
414                     return self.dataset.create(values).pipe(function(r) {
415                         return self.on_created(r, undefined, prepend_on_create);
416                     }).then(success);
417                 } else {
418                     return self.dataset.write(self.datarecord.id, values, {}).pipe(function(r) {
419                         return self.on_saved(r);
420                     }).then(success);
421                 }
422             }
423             } catch (e) {
424                 console.error(e);
425                 return $.Deferred().reject();
426             }
427         };
428         this.mutating_lock = this.mutating_lock.pipe(action, action);
429         return this.mutating_lock;
430     },
431     switch_readonly: function() {
432     },
433     switch_editable: function() {
434     },
435     on_invalid: function() {
436         var msg = "<ul>";
437         _.each(this.fields, function(f) {
438             if (!f.is_valid()) {
439                 msg += "<li>" + f.string + "</li>";
440             }
441         });
442         msg += "</ul>";
443         this.do_warn("The following fields are invalid :", msg);
444     },
445     on_saved: function(r, success) {
446         if (!r.result) {
447             // should not happen in the server, but may happen for internal purpose
448             return $.Deferred().reject();
449         } else {
450             this.reload();
451             return $.when(r).then(success);
452         }
453     },
454     /**
455      * Updates the form' dataset to contain the new record:
456      *
457      * * Adds the newly created record to the current dataset (at the end by
458      *   default)
459      * * Selects that record (sets the dataset's index to point to the new
460      *   record's id).
461      * * Updates the pager and sidebar displays
462      *
463      * @param {Object} r
464      * @param {Function} success callback to execute after having updated the dataset
465      * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end
466      */
467     on_created: function(r, success, prepend_on_create) {
468         if (!r.result) {
469             // should not happen in the server, but may happen for internal purpose
470             return $.Deferred().reject();
471         } else {
472             this.datarecord.id = r.result;
473             if (!prepend_on_create) {
474                 this.dataset.ids.push(this.datarecord.id);
475                 this.dataset.index = this.dataset.ids.length - 1;
476             } else {
477                 this.dataset.ids.unshift(this.datarecord.id);
478                 this.dataset.index = 0;
479             }
480             this.do_update_pager();
481             if (this.sidebar) {
482                 this.sidebar.attachments.do_update();
483             }
484             console.debug("The record has been created with id #" + this.datarecord.id);
485             this.reload();
486             return $.when(_.extend(r, {created: true})).then(success);
487         }
488     },
489     on_action: function (action) {
490         console.debug('Executing action', action);
491     },
492     do_cancel: function () {
493         console.debug("Cancelling form");
494     },
495     reload: function() {
496         var self = this;
497         var act = function() {
498             if (self.dataset.index == null || self.dataset.index < 0) {
499                 return $.when(self.on_button_new());
500             } else {
501                 return self.dataset.read_index(_.keys(self.fields_view.fields), self.on_record_loaded);
502             }
503         };
504         this.reload_lock = this.reload_lock.pipe(act, act);
505         return this.reload_lock;
506     },
507     get_fields_values: function() {
508         var values = {};
509         _.each(this.fields, function(value, key) {
510             var val = value.get_value();
511             values[key] = val;
512         });
513         return values;
514     },
515     get_selected_ids: function() {
516         var id = this.dataset.ids[this.dataset.index];
517         return id ? [id] : [];
518     },
519     recursive_save: function() {
520         var self = this;
521         return $.when(this.do_save()).pipe(function(res) {
522             if (self.dataset.parent_view)
523                 return self.dataset.parent_view.recursive_save();
524         });
525     },
526     is_interactible_record: function() {
527         var id = this.datarecord.id;
528         if (!id) {
529             if (this.options.not_interactible_on_create)
530                 return false;
531         } else if (typeof(id) === "string") {
532             if(openerp.web.BufferedDataSet.virtual_id_regex.test(id))
533                 return false;
534         }
535         return true;
536     }
537 });
538 openerp.web.FormDialog = openerp.web.Dialog.extend({
539     init: function(parent, options, view_id, dataset) {
540         this._super(parent, options);
541         this.dataset = dataset;
542         this.view_id = view_id;
543         return this;
544     },
545     start: function() {
546         this._super();
547         this.form = new openerp.web.FormView(this, this.dataset, this.view_id, {
548             sidebar: false,
549             pager: false
550         });
551         this.form.appendTo(this.$element);
552         this.form.on_created.add_last(this.on_form_dialog_saved);
553         this.form.on_saved.add_last(this.on_form_dialog_saved);
554         return this;
555     },
556     select_id: function(id) {
557         if (this.form.dataset.select_id(id)) {
558             return this.form.do_show();
559         } else {
560             this.do_warn("Could not find id in dataset");
561             return $.Deferred().reject();
562         }
563     },
564     on_form_dialog_saved: function(r) {
565         this.close();
566     }
567 });
568
569 /** @namespace */
570 openerp.web.form = {};
571
572 openerp.web.form.SidebarAttachments = openerp.web.Widget.extend({
573     init: function(parent, form_view) {
574         var $section = parent.add_section(_t('Attachments'), 'attachments');
575         this.$div = $('<div class="oe-sidebar-attachments"></div>');
576         $section.append(this.$div);
577
578         this._super(parent, $section.attr('id'));
579         this.view = form_view;
580     },
581     do_update: function() {
582         if (!this.view.datarecord.id) {
583             this.on_attachments_loaded([]);
584         } else {
585             (new openerp.web.DataSetSearch(
586                 this, 'ir.attachment', this.view.dataset.get_context(),
587                 [
588                     ['res_model', '=', this.view.dataset.model],
589                     ['res_id', '=', this.view.datarecord.id],
590                     ['type', 'in', ['binary', 'url']]
591                 ])).read_slice(['name', 'url', 'type'], {}, this.on_attachments_loaded);
592         }
593     },
594     on_attachments_loaded: function(attachments) {
595         this.attachments = attachments;
596         this.$div.html(QWeb.render('FormView.sidebar.attachments', this));
597         this.$element.find('.oe-binary-file').change(this.on_attachment_changed);
598         this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete);
599     },
600     on_attachment_changed: function(e) {
601         window[this.element_id + '_iframe'] = this.do_update;
602         var $e = $(e.target);
603         if ($e.val() != '') {
604             this.$element.find('form.oe-binary-form').submit();
605             $e.parent().find('input[type=file]').attr('disabled', 'true');
606             $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle();
607         }
608     },
609     on_attachment_delete: function(e) {
610         var self = this, $e = $(e.currentTarget);
611         var name = _.trim($e.parent().find('a.oe-sidebar-attachments-link').text());
612         if (confirm("Do you really want to delete the attachment " + name + " ?")) {
613             this.rpc('/web/dataset/unlink', {
614                 model: 'ir.attachment',
615                 ids: [parseInt($e.attr('data-id'))]
616             }, function(r) {
617                 $e.parent().remove();
618                 self.do_notify("Delete an attachment", "The attachment '" + name + "' has been deleted");
619             });
620         }
621     }
622 });
623
624 openerp.web.form.compute_domain = function(expr, fields) {
625     var stack = [];
626     for (var i = expr.length - 1; i >= 0; i--) {
627         var ex = expr[i];
628         if (ex.length == 1) {
629             var top = stack.pop();
630             switch (ex) {
631                 case '|':
632                     stack.push(stack.pop() || top);
633                     continue;
634                 case '&':
635                     stack.push(stack.pop() && top);
636                     continue;
637                 case '!':
638                     stack.push(!top);
639                     continue;
640                 default:
641                     throw new Error('Unknown domain operator ' + ex);
642             }
643         }
644
645         var field = fields[ex[0]];
646         if (!field) {
647             throw new Error("Domain references unknown field : " + ex[0]);
648         }
649         var field_value = field.get_value ? fields[ex[0]].get_value() : fields[ex[0]].value;
650         var op = ex[1];
651         var val = ex[2];
652
653         switch (op.toLowerCase()) {
654             case '=':
655             case '==':
656                 stack.push(field_value == val);
657                 break;
658             case '!=':
659             case '<>':
660                 stack.push(field_value != val);
661                 break;
662             case '<':
663                 stack.push(field_value < val);
664                 break;
665             case '>':
666                 stack.push(field_value > val);
667                 break;
668             case '<=':
669                 stack.push(field_value <= val);
670                 break;
671             case '>=':
672                 stack.push(field_value >= val);
673                 break;
674             case 'in':
675                 stack.push(_(val).contains(field_value));
676                 break;
677             case 'not in':
678                 stack.push(!_(val).contains(field_value));
679                 break;
680             default:
681                 console.log("Unsupported operator in modifiers :", op);
682         }
683     }
684     return _.all(stack, _.identity);
685 };
686
687 openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{
688     template: 'Widget',
689     identifier_prefix: 'formview-widget-',
690     /**
691      * @constructs openerp.web.form.Widget
692      * @extends openerp.web.Widget
693      *
694      * @param view
695      * @param node
696      */
697     init: function(view, node) {
698         this.view = view;
699         this.node = node;
700         this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
701         this.always_invisible = (this.modifiers.invisible && this.modifiers.invisible === true);
702         this.type = this.type || node.tag;
703         this.element_name = this.element_name || this.type;
704         this.element_class = [
705             'formview', this.view.view_id, this.element_name,
706             this.view.widgets_counter++].join("_");
707
708         this._super(view);
709
710         this.view.widgets[this.element_class] = this;
711         this.children = node.children;
712         this.colspan = parseInt(node.attrs.colspan || 1, 10);
713         this.decrease_max_width = 0;
714
715         this.string = this.string || node.attrs.string;
716         this.help = this.help || node.attrs.help;
717         this.invisible = this.modifiers['invisible'] === true;
718         this.classname = 'oe_form_' + this.type;
719
720         this.align = parseFloat(this.node.attrs.align);
721         if (isNaN(this.align) || this.align === 1) {
722             this.align = 'right';
723         } else if (this.align === 0) {
724             this.align = 'left';
725         } else {
726             this.align = 'center';
727         }
728
729
730         this.width = this.node.attrs.width;
731     },
732     start: function() {
733         this.$element = this.view.$element.find(
734             '.' + this.element_class.replace(/[^\r\n\f0-9A-Za-z_-]/g, "\\$&"));
735     },
736     process_modifiers: function() {
737         var compute_domain = openerp.web.form.compute_domain;
738         for (var a in this.modifiers) {
739             this[a] = compute_domain(this.modifiers[a], this.view.fields);
740         }
741     },
742     update_dom: function() {
743         this.$element.toggle(!this.invisible);
744     },
745     render: function() {
746         var template = this.template;
747         return QWeb.render(template, { "widget": this });
748     },
749     _build_view_fields_values: function() {
750         var a_dataset = this.view.dataset;
751         var fields_values = this.view.get_fields_values();
752         var parent_values = a_dataset.parent_view ? a_dataset.parent_view.get_fields_values() : {};
753         fields_values.parent = parent_values;
754         return fields_values;
755     },
756     _build_eval_context: function() {
757         var a_dataset = this.view.dataset;
758         return new openerp.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
759     },
760     /**
761      * Builds a new context usable for operations related to fields by merging
762      * the fields'context with the action's context.
763      */
764     build_context: function() {
765         var f_context = (this.field || {}).context || {};
766         if (!!f_context.__ref) {
767             var fields_values = this._build_eval_context();
768             f_context = new openerp.web.CompoundDomain(f_context).set_eval_context(fields_values);
769         }
770         // maybe the default_get should only be used when we do a default_get?
771         var v_contexts = _.compact([this.node.attrs.default_get || null,
772             this.node.attrs.context || null]);
773         var v_context = new openerp.web.CompoundContext();
774         _.each(v_contexts, function(x) {v_context.add(x);});
775         if (_.detect(v_contexts, function(x) {return !!x.__ref;})) {
776             var fields_values = this._build_eval_context();
777             v_context.set_eval_context(fields_values);
778         }
779         // if there is a context on the node, overrides the model's context
780         var ctx = v_contexts.length > 0 ? v_context : f_context;
781         return ctx;
782     },
783     build_domain: function() {
784         var f_domain = this.field.domain || [];
785         var n_domain = this.node.attrs.domain || null;
786         // if there is a domain on the node, overrides the model's domain
787         var final_domain = n_domain !== null ? n_domain : f_domain;
788         if (!(final_domain instanceof Array)) {
789             var fields_values = this._build_eval_context();
790             final_domain = new openerp.web.CompoundDomain(final_domain).set_eval_context(fields_values);
791         }
792         return final_domain;
793     }
794 });
795
796 openerp.web.form.WidgetFrame = openerp.web.form.Widget.extend({
797     template: 'WidgetFrame',
798     init: function(view, node) {
799         this._super(view, node);
800         this.columns = parseInt(node.attrs.col || 4, 10);
801         this.x = 0;
802         this.y = 0;
803         this.table = [];
804         this.add_row();
805         for (var i = 0; i < node.children.length; i++) {
806             var n = node.children[i];
807             if (n.tag == "newline") {
808                 this.add_row();
809             } else {
810                 this.handle_node(n);
811             }
812         }
813         this.set_row_cells_with(this.table[this.table.length - 1]);
814     },
815     add_row: function(){
816         if (this.table.length) {
817             this.set_row_cells_with(this.table[this.table.length - 1]);
818         }
819         var row = [];
820         this.table.push(row);
821         this.x = 0;
822         this.y += 1;
823         return row;
824     },
825     set_row_cells_with: function(row) {
826         var bypass = 0,
827             max_width = 100,
828             row_length = row.length;
829         for (var i = 0; i < row.length; i++) {
830             if (row[i].always_invisible) {
831                 row_length--;
832             } else {
833                 bypass += row[i].width === undefined ? 0 : 1;
834                 max_width -= row[i].decrease_max_width;
835             }
836         }
837         var size_unit = Math.round(max_width / (this.columns - bypass)),
838             colspan_sum = 0;
839         for (var i = 0; i < row.length; i++) {
840             var w = row[i];
841             if (w.always_invisible) {
842                 continue;
843             }
844             colspan_sum += w.colspan;
845             if (w.width === undefined) {
846                 var width = (i === row_length - 1 && colspan_sum === this.columns) ? max_width : Math.round(size_unit * w.colspan);
847                 max_width -= width;
848                 w.width = width + '%';
849             }
850         }
851     },
852     handle_node: function(node) {
853         var type = {};
854         if (node.tag == 'field') {
855             type = this.view.fields_view.fields[node.attrs.name] || {};
856             if (node.attrs.widget == 'statusbar') {
857                 // This way we can retain backward compatibility between addons and old clients
858                 node.attrs.nolabel = '1';
859             }
860         }
861         var widget = new (this.view.registry.get_any(
862                 [node.attrs.widget, type.type, node.tag])) (this.view, node);
863         if (node.tag == 'field') {
864             if (!this.view.default_focus_field || node.attrs.default_focus == '1') {
865                 this.view.default_focus_field = widget;
866             }
867             if (node.attrs.nolabel != '1') {
868                 var label = new (this.view.registry.get_object('label')) (this.view, node);
869                 label["for"] = widget;
870                 this.add_widget(label, widget.colspan + 1);
871             }
872         }
873         this.add_widget(widget);
874     },
875     add_widget: function(widget, colspan) {
876         var current_row = this.table[this.table.length - 1];
877         if (!widget.always_invisible) {
878             colspan = colspan || widget.colspan;
879             if (current_row.length && (this.x + colspan) > this.columns) {
880                 current_row = this.add_row();
881             }
882             this.x += widget.colspan;
883         }
884         current_row.push(widget);
885         return widget;
886     }
887 });
888
889 openerp.web.form.WidgetNotebook = openerp.web.form.Widget.extend({
890     template: 'WidgetNotebook',
891     init: function(view, node) {
892         this._super(view, node);
893         this.pages = [];
894         for (var i = 0; i < node.children.length; i++) {
895             var n = node.children[i];
896             if (n.tag == "page") {
897                 var page = new (this.view.registry.get_object('notebookpage'))(
898                         this.view, n, this, this.pages.length);
899                 this.pages.push(page);
900             }
901         }
902     },
903     start: function() {
904         var self = this;
905         this._super.apply(this, arguments);
906         this.$element.find('> ul > li').each(function (index, tab_li) {
907             var page = self.pages[index],
908                 id = _.uniqueId(self.element_name + '-');
909             page.element_id = id;
910             $(tab_li).find('a').attr('href', '#' + id);
911         });
912         this.$element.find('> div').each(function (index, page) {
913             page.id = self.pages[index].element_id;
914         });
915         this.$element.tabs();
916         this.view.on_button_new.add_first(this.do_select_first_visible_tab);
917     },
918     do_select_first_visible_tab: function() {
919         for (var i = 0; i < this.pages.length; i++) {
920             var page = this.pages[i];
921             if (page.invisible === false) {
922                 this.$element.tabs('select', page.index);
923                 break;
924             }
925         }
926     }
927 });
928
929 openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({
930     template: 'WidgetNotebookPage',
931     init: function(view, node, notebook, index) {
932         this.notebook = notebook;
933         this.index = index;
934         this.element_name = 'page_' + index;
935         this._super(view, node);
936     },
937     start: function() {
938         this._super.apply(this, arguments);
939         this.$element_tab = this.notebook.$element.find(
940                 '> ul > li:eq(' + this.index + ')');
941     },
942     update_dom: function() {
943         if (this.invisible && this.index === this.notebook.$element.tabs('option', 'selected')) {
944             this.notebook.do_select_first_visible_tab();
945         }
946         this.$element_tab.toggle(!this.invisible);
947         this.$element.toggle(!this.invisible);
948     }
949 });
950
951 openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({
952     template: 'WidgetSeparator',
953     init: function(view, node) {
954         this._super(view, node);
955         this.orientation = node.attrs.orientation || 'horizontal';
956         if (this.orientation === 'vertical') {
957             this.width = '1';
958         }
959         this.classname += '_' + this.orientation;
960     }
961 });
962
963 openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
964     template: 'WidgetButton',
965     init: function(view, node) {
966         this._super(view, node);
967         this.force_disabled = false;
968         if (this.string) {
969             // We don't have button key bindings in the webclient
970             this.string = this.string.replace(/_/g, '');
971         }
972         if (node.attrs.default_focus == '1') {
973             // TODO fme: provide enter key binding to widgets
974             this.view.default_focus_button = this;
975         }
976     },
977     start: function() {
978         this._super.apply(this, arguments);
979         this.$element.find("button").click(this.on_click);
980     },
981     on_click: function() {
982         var self = this;
983         this.force_disabled = true;
984         this.check_disable();
985         this.execute_action().always(function() {
986             self.force_disabled = false;
987             self.check_disable();
988         });
989     },
990     execute_action: function() {
991         var self = this;
992         var exec_action = function() {
993             if (self.node.attrs.confirm) {
994                 var def = $.Deferred();
995                 var dialog = $('<div>' + self.node.attrs.confirm + '</div>').dialog({
996                     title: 'Confirm',
997                     modal: true,
998                     buttons: {
999                         Ok: function() {
1000                             self.on_confirmed().then(function() {
1001                                 def.resolve();
1002                             });
1003                             $(this).dialog("close");
1004                         },
1005                         Cancel: function() {
1006                             def.resolve();
1007                             $(this).dialog("close");
1008                         }
1009                     }
1010                 });
1011                 return def.promise();
1012             } else {
1013                 return self.on_confirmed();
1014             }
1015         };
1016         if (!this.node.attrs.special) {
1017             return this.view.recursive_save().pipe(exec_action);
1018         } else {
1019             return exec_action();
1020         }
1021     },
1022     on_confirmed: function() {
1023         var self = this;
1024
1025         var context = this.node.attrs.context;
1026         if (context && context.__ref) {
1027             context = new openerp.web.CompoundContext(context);
1028             context.set_eval_context(this._build_eval_context());
1029         }
1030
1031         return this.view.do_execute_action(
1032             _.extend({}, this.node.attrs, {context: context}),
1033             this.view.dataset, this.view.datarecord.id, function () {
1034                 self.view.reload();
1035             });
1036     },
1037     update_dom: function() {
1038         this._super();
1039         this.check_disable();
1040     },
1041     check_disable: function() {
1042         if (this.readonly || this.force_disabled || !this.view.is_interactible_record()) {
1043             this.$element.find("button").attr("disabled", "disabled");
1044             this.$element.find("button").css("color", "grey");
1045         } else {
1046             this.$element.find("button").removeAttr("disabled");
1047             this.$element.find("button").css("color", "");
1048         }
1049     }
1050 });
1051
1052 openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({
1053     template: 'WidgetLabel',
1054     init: function(view, node) {
1055         this.element_name = 'label_' + node.attrs.name;
1056
1057         this._super(view, node);
1058
1059         if (this.node.tag == 'label' && (this.align === 'left' || this.node.attrs.colspan || (this.string && this.string.length > 32))) {
1060             this.template = "WidgetParagraph";
1061             this.colspan = parseInt(this.node.attrs.colspan || 1, 10);
1062         } else {
1063             this.colspan = 1;
1064             this.width = '1%';
1065             this.decrease_max_width = 1;
1066             this.nowrap = true;
1067         }
1068     },
1069     render: function () {
1070         if (this['for'] && this.type !== 'label') {
1071             return QWeb.render(this.template, {widget: this['for']});
1072         }
1073         // Actual label widgets should not have a false and have type label
1074         return QWeb.render(this.template, {widget: this});
1075     },
1076     start: function() {
1077         this._super();
1078         var self = this;
1079         this.$element.find("label").dblclick(function() {
1080             var widget = self['for'] || self;
1081             console.log(widget.element_class , widget);
1082             window.w = widget;
1083         });
1084     }
1085 });
1086
1087 openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.form.Field# */{
1088     /**
1089      * @constructs openerp.web.form.Field
1090      * @extends openerp.web.form.Widget
1091      *
1092      * @param view
1093      * @param node
1094      */
1095     init: function(view, node) {
1096         this.name = node.attrs.name;
1097         this.value = undefined;
1098         view.fields[this.name] = this;
1099         this.type = node.attrs.widget || view.fields_view.fields[node.attrs.name].type;
1100         this.element_name = "field_" + this.name + "_" + this.type;
1101
1102         this._super(view, node);
1103
1104         if (node.attrs.nolabel != '1' && this.colspan > 1) {
1105             this.colspan--;
1106         }
1107         this.field = view.fields_view.fields[node.attrs.name] || {};
1108         this.string = node.attrs.string || this.field.string;
1109         this.help = node.attrs.help || this.field.help;
1110         this.nolabel = (this.field.nolabel || node.attrs.nolabel) === '1';
1111         this.readonly = this.modifiers['readonly'] === true;
1112         this.required = this.modifiers['required'] === true;
1113         this.invalid = false;
1114         this.dirty = false;
1115
1116         this.classname = 'oe_form_field_' + this.type;
1117     },
1118     start: function() {
1119         this._super.apply(this, arguments);
1120         if (this.field.translate) {
1121             this.view.translatable_fields.push(this);
1122             this.$element.find('.oe_field_translate').click(this.on_translate);
1123         }
1124     },
1125     set_value: function(value) {
1126         this.value = value;
1127         this.invalid = false;
1128         this.update_dom();
1129         this.on_value_changed();
1130     },
1131     set_value_from_ui: function() {
1132         this.on_value_changed();
1133     },
1134     on_value_changed: function() {
1135     },
1136     on_translate: function() {
1137         this.view.open_translate_dialog(this);
1138     },
1139     get_value: function() {
1140         return this.value;
1141     },
1142     is_valid: function() {
1143         return !this.invalid;
1144     },
1145     is_dirty: function() {
1146         return this.dirty && !this.readonly;
1147     },
1148     get_on_change_value: function() {
1149         return this.get_value();
1150     },
1151     update_dom: function() {
1152         this._super.apply(this, arguments);
1153         if (this.field.translate) {
1154             this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id);
1155         }
1156         if (!this.disable_utility_classes) {
1157             this.$element.toggleClass('disabled', this.readonly);
1158             this.$element.toggleClass('required', this.required);
1159             if (this.view.show_invalid) {
1160                 this.$element.toggleClass('invalid', !this.is_valid());
1161             }
1162         }
1163     },
1164     on_ui_change: function() {
1165         this.dirty = this.view.dirty_for_user = true;
1166         this.validate();
1167         if (this.is_valid()) {
1168             this.set_value_from_ui();
1169             this.view.do_onchange(this);
1170             this.view.on_form_changed();
1171         } else {
1172             this.update_dom();
1173         }
1174     },
1175     validate: function() {
1176         this.invalid = false;
1177     },
1178     focus: function() {
1179     }
1180 });
1181
1182 openerp.web.form.FieldChar = openerp.web.form.Field.extend({
1183     template: 'FieldChar',
1184     init: function (view, node) {
1185         this._super(view, node);
1186         this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
1187     },
1188     start: function() {
1189         this._super.apply(this, arguments);
1190         this.$element.find('input').change(this.on_ui_change);
1191     },
1192     set_value: function(value) {
1193         this._super.apply(this, arguments);
1194         var show_value = openerp.web.format_value(value, this, '');
1195         this.$element.find('input').val(show_value);
1196         return show_value;
1197     },
1198     update_dom: function() {
1199         this._super.apply(this, arguments);
1200         this.$element.find('input').attr('disabled', this.readonly);
1201     },
1202     set_value_from_ui: function() {
1203         this.value = openerp.web.parse_value(this.$element.find('input').val(), this);
1204         this._super();
1205     },
1206     validate: function() {
1207         this.invalid = false;
1208         try {
1209             var value = openerp.web.parse_value(this.$element.find('input').val(), this, '');
1210             this.invalid = this.required && value === '';
1211         } catch(e) {
1212             this.invalid = true;
1213         }
1214     },
1215     focus: function() {
1216         this.$element.find('input').focus();
1217     }
1218 });
1219
1220 openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({
1221     template: 'FieldEmail',
1222     start: function() {
1223         this._super.apply(this, arguments);
1224         this.$element.find('button').click(this.on_button_clicked);
1225     },
1226     on_button_clicked: function() {
1227         if (!this.value || !this.is_valid()) {
1228             this.do_warn("E-mail error", "Can't send email to invalid e-mail address");
1229         } else {
1230             location.href = 'mailto:' + this.value;
1231         }
1232     }
1233 });
1234
1235 openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({
1236     template: 'FieldUrl',
1237     start: function() {
1238         this._super.apply(this, arguments);
1239         this.$element.find('button').click(this.on_button_clicked);
1240     },
1241     on_button_clicked: function() {
1242         if (!this.value) {
1243             this.do_warn("Resource error", "This resource is empty");
1244         } else {
1245             window.open(this.value);
1246         }
1247     }
1248 });
1249
1250 openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({
1251     init: function (view, node) {
1252         this._super(view, node);
1253         if (node.attrs.digits) {
1254             this.parse_digits(node.attrs.digits);
1255         } else {
1256             this.digits = view.fields_view.fields[node.attrs.name].digits;
1257         }
1258     },
1259     parse_digits: function (digits_attr) {
1260         // could use a Python parser instead.
1261         var match = /^\s*[\(\[](\d+),\s*(\d+)/.exec(digits_attr);
1262         return [parseInt(match[1], 10), parseInt(match[2], 10)];
1263     },
1264     set_value: function(value) {
1265         if (value === false || value === undefined) {
1266             // As in GTK client, floats default to 0
1267             value = 0;
1268             this.dirty = true;
1269         }
1270         this._super.apply(this, [value]);
1271     }
1272 });
1273
1274 openerp.web.DateTimeWidget = openerp.web.Widget.extend({
1275     template: "web.datetimepicker",
1276     jqueryui_object: 'datetimepicker',
1277     type_of_date: "datetime",
1278     start: function() {
1279         var self = this;
1280         this.$element.find('input').change(this.on_change);
1281         this.picker({
1282             onSelect: this.on_picker_select,
1283             changeMonth: true,
1284             changeYear: true,
1285             showWeek: true,
1286             showButtonPanel: false
1287         });
1288         this.$element.find('img.oe_datepicker_trigger').click(function() {
1289             if (!self.readonly) {
1290                 self.picker('setDate', self.value ? openerp.web.auto_str_to_date(self.value) : new Date());
1291                 self.$element.find('.oe_datepicker').toggle();
1292             }
1293         });
1294         this.$element.find('.ui-datepicker-inline').removeClass('ui-widget-content ui-corner-all');
1295         this.$element.find('button.oe_datepicker_close').click(function() {
1296             self.$element.find('.oe_datepicker').hide();
1297         });
1298         this.set_readonly(false);
1299         this.value = false;
1300     },
1301     picker: function() {
1302         return $.fn[this.jqueryui_object].apply(this.$element.find('.oe_datepicker_container'), arguments);
1303     },
1304     on_picker_select: function(text, instance) {
1305         var date = this.picker('getDate');
1306         this.$element.find('input').val(date ? this.format_client(date) : '').change();
1307     },
1308     set_value: function(value) {
1309         this.value = value;
1310         this.$element.find('input').val(value ? this.format_client(value) : '');
1311     },
1312     get_value: function() {
1313         return this.value;
1314     },
1315     set_value_from_ui: function() {
1316         var value = this.$element.find('input').val() || false;
1317         this.value = this.parse_client(value);
1318     },
1319     set_readonly: function(readonly) {
1320         this.readonly = readonly;
1321         this.$element.find('input').attr('disabled', this.readonly);
1322         this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
1323     },
1324     is_valid: function(required) {
1325         var value = this.$element.find('input').val();
1326         if (value === "") {
1327             return !required;
1328         } else {
1329             try {
1330                 this.parse_client(value);
1331                 return true;
1332             } catch(e) {
1333                 return false;
1334             }
1335         }
1336     },
1337     focus: function() {
1338         this.$element.find('input').focus();
1339     },
1340     parse_client: function(v) {
1341         return openerp.web.parse_value(v, {"widget": this.type_of_date});
1342     },
1343     format_client: function(v) {
1344         return openerp.web.format_value(v, {"widget": this.type_of_date});
1345     },
1346     on_change: function() {
1347         if (this.is_valid()) {
1348             this.set_value_from_ui();
1349         }
1350     }
1351 });
1352
1353 openerp.web.DateWidget = openerp.web.DateTimeWidget.extend({
1354     jqueryui_object: 'datepicker',
1355     type_of_date: "date",
1356     on_picker_select: function(text, instance) {
1357         this._super(text, instance);
1358         this.$element.find('.oe_datepicker').hide();
1359     }
1360 });
1361
1362 openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({
1363     template: "EmptyComponent",
1364     build_widget: function() {
1365         return new openerp.web.DateTimeWidget(this);
1366     },
1367     start: function() {
1368         var self = this;
1369         this._super.apply(this, arguments);
1370         this.datewidget = this.build_widget();
1371         this.datewidget.on_change.add(this.on_ui_change);
1372         this.datewidget.appendTo(this.$element);
1373     },
1374     set_value: function(value) {
1375         this._super(value);
1376         this.datewidget.set_value(value);
1377     },
1378     get_value: function() {
1379         return this.datewidget.get_value();
1380     },
1381     update_dom: function() {
1382         this._super.apply(this, arguments);
1383         this.datewidget.set_readonly(this.readonly);
1384     },
1385     validate: function() {
1386         this.invalid = !this.datewidget.is_valid(this.required);
1387     },
1388     focus: function() {
1389         this.datewidget.focus();
1390     }
1391 });
1392
1393 openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({
1394     build_widget: function() {
1395         return new openerp.web.DateWidget(this);
1396     }
1397 });
1398
1399 openerp.web.form.FieldText = openerp.web.form.Field.extend({
1400     template: 'FieldText',
1401     start: function() {
1402         this._super.apply(this, arguments);
1403         this.$element.find('textarea').change(this.on_ui_change);
1404     },
1405     set_value: function(value) {
1406         this._super.apply(this, arguments);
1407         var show_value = openerp.web.format_value(value, this, '');
1408         this.$element.find('textarea').val(show_value);
1409     },
1410     update_dom: function() {
1411         this._super.apply(this, arguments);
1412         this.$element.find('textarea').attr('disabled', this.readonly);
1413     },
1414     set_value_from_ui: function() {
1415         this.value = openerp.web.parse_value(this.$element.find('textarea').val(), this);
1416         this._super();
1417     },
1418     validate: function() {
1419         this.invalid = false;
1420         try {
1421             var value = openerp.web.parse_value(this.$element.find('textarea').val(), this, '');
1422             this.invalid = this.required && value === '';
1423         } catch(e) {
1424             this.invalid = true;
1425         }
1426     },
1427     focus: function() {
1428         this.$element.find('textarea').focus();
1429     }
1430 });
1431
1432 openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({
1433     template: 'FieldBoolean',
1434     start: function() {
1435         var self = this;
1436         this._super.apply(this, arguments);
1437         this.$element.find('input').click(self.on_ui_change);
1438     },
1439     set_value: function(value) {
1440         this._super.apply(this, arguments);
1441         this.$element.find('input')[0].checked = value;
1442     },
1443     set_value_from_ui: function() {
1444         this.value = this.$element.find('input').is(':checked');
1445         this._super();
1446     },
1447     update_dom: function() {
1448         this._super.apply(this, arguments);
1449         this.$element.find('input').attr('disabled', this.readonly);
1450     },
1451     focus: function() {
1452         this.$element.find('input').focus();
1453     }
1454 });
1455
1456 openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({
1457     template: 'FieldProgressBar',
1458     start: function() {
1459         this._super.apply(this, arguments);
1460         this.$element.find('div').progressbar({
1461             value: this.value,
1462             disabled: this.readonly
1463         });
1464     },
1465     set_value: function(value) {
1466         this._super.apply(this, arguments);
1467         var show_value = Number(value);
1468         if (isNaN(show_value)) {
1469             show_value = 0;
1470         }
1471         this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
1472     }
1473 });
1474
1475 openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({
1476 // to replace view editor
1477 });
1478
1479 openerp.web.form.FieldSelection = openerp.web.form.Field.extend({
1480     template: 'FieldSelection',
1481     init: function(view, node) {
1482         var self = this;
1483         this._super(view, node);
1484         this.values = this.field.selection;
1485         _.each(this.values, function(v, i) {
1486             if (v[0] === false && v[1] === '') {
1487                 self.values.splice(i, 1);
1488             }
1489         });
1490         this.values.unshift([false, '']);
1491     },
1492     start: function() {
1493         // Flag indicating whether we're in an event chain containing a change
1494         // event on the select, in order to know what to do on keyup[RETURN]:
1495         // * If the user presses [RETURN] as part of changing the value of a
1496         //   selection, we should just let the value change and not let the
1497         //   event broadcast further (e.g. to validating the current state of
1498         //   the form in editable list view, which would lead to saving the
1499         //   current row or switching to the next one)
1500         // * If the user presses [RETURN] with a select closed (side-effect:
1501         //   also if the user opened the select and pressed [RETURN] without
1502         //   changing the selected value), takes the action as validating the
1503         //   row
1504         var ischanging = false;
1505         this._super.apply(this, arguments);
1506         this.$element.find('select')
1507             .change(this.on_ui_change)
1508             .change(function () { ischanging = true; })
1509             .click(function () { ischanging = false; })
1510             .keyup(function (e) {
1511                 if (e.which !== 13 || !ischanging) { return; }
1512                 e.stopPropagation();
1513                 ischanging = false;
1514             });
1515     },
1516     set_value: function(value) {
1517         value = value === null ? false : value;
1518         value = value instanceof Array ? value[0] : value;
1519         this._super(value);
1520         var index = 0;
1521         for (var i = 0, ii = this.values.length; i < ii; i++) {
1522             if (this.values[i][0] === value) index = i;
1523         }
1524         this.$element.find('select')[0].selectedIndex = index;
1525     },
1526     set_value_from_ui: function() {
1527         this.value = this.values[this.$element.find('select')[0].selectedIndex][0];
1528         this._super();
1529     },
1530     update_dom: function() {
1531         this._super.apply(this, arguments);
1532         this.$element.find('select').attr('disabled', this.readonly);
1533     },
1534     validate: function() {
1535         var value = this.values[this.$element.find('select')[0].selectedIndex];
1536         this.invalid = !(value && !(this.required && value[0] === false));
1537     },
1538     focus: function() {
1539         this.$element.find('select').focus();
1540     }
1541 });
1542
1543 // jquery autocomplete tweak to allow html
1544 (function() {
1545     var proto = $.ui.autocomplete.prototype,
1546         initSource = proto._initSource;
1547
1548     function filter( array, term ) {
1549         var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
1550         return $.grep( array, function(value) {
1551             return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() );
1552         });
1553     }
1554
1555     $.extend( proto, {
1556         _initSource: function() {
1557             if ( this.options.html && $.isArray(this.options.source) ) {
1558                 this.source = function( request, response ) {
1559                     response( filter( this.options.source, request.term ) );
1560                 };
1561             } else {
1562                 initSource.call( this );
1563             }
1564         },
1565
1566         _renderItem: function( ul, item) {
1567             return $( "<li></li>" )
1568                 .data( "item.autocomplete", item )
1569                 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
1570                 .appendTo( ul );
1571         }
1572     });
1573 })();
1574
1575 openerp.web.form.dialog = function(content, options) {
1576     options = _.extend({
1577         autoOpen: true,
1578         width: '90%',
1579         height: '90%',
1580         min_width: '800px',
1581         min_height: '600px'
1582     }, options || {});
1583     options.autoOpen = true;
1584     var dialog = new openerp.web.Dialog(null, options);
1585     dialog.$dialog = $(content).dialog(dialog.dialog_options);
1586     return dialog.$dialog;
1587 };
1588
1589 openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
1590     template: 'FieldMany2One',
1591     init: function(view, node) {
1592         this._super(view, node);
1593         this.limit = 7;
1594         this.value = null;
1595         this.cm_id = _.uniqueId('m2o_cm_');
1596         this.last_search = [];
1597         this.tmp_value = undefined;
1598     },
1599     start: function() {
1600         this._super();
1601         var self = this;
1602         this.$input = this.$element.find("input");
1603         this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
1604         this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
1605
1606         // context menu
1607         var init_context_menu_def = $.Deferred().then(function(e) {
1608             var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context());
1609             rdataset.call("get", ['action', 'client_action_relate',
1610                 [[self.field.relation, false]], false, rdataset.get_context()], false, 0)
1611                 .then(function(result) {
1612                 self.related_entries = result;
1613
1614                 var $cmenu = $("#" + self.cm_id);
1615                 $cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self}));
1616                 var bindings = {};
1617                 bindings[self.cm_id + "_search"] = function() {
1618                     self._search_create_popup("search");
1619                 };
1620                 bindings[self.cm_id + "_create"] = function() {
1621                     self._search_create_popup("form");
1622                 };
1623                 bindings[self.cm_id + "_open"] = function() {
1624                     if (!self.value) {
1625                         return;
1626                     }
1627                     var pop = new openerp.web.form.FormOpenPopup(self.view);
1628                     pop.show_element(self.field.relation, self.value[0],self.build_context(), {});
1629                     pop.on_write_completed.add_last(function() {
1630                         self.set_value(self.value[0]);
1631                     });
1632                 };
1633                 _.each(_.range(self.related_entries.length), function(i) {
1634                     bindings[self.cm_id + "_related_" + i] = function() {
1635                         self.open_related(self.related_entries[i]);
1636                     };
1637                 });
1638                 var cmenu = self.$menu_btn.contextMenu(self.cm_id, {'leftClickToo': true,
1639                     bindings: bindings, itemStyle: {"color": ""},
1640                     onContextMenu: function() {
1641                         if(self.value) {
1642                             $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").removeClass("oe-m2o-disabled-cm");
1643                         } else {
1644                             $("#" + self.cm_id + " .oe_m2o_menu_item_mandatory").addClass("oe-m2o-disabled-cm");
1645                         }
1646                         if (!self.readonly) {
1647                             $("#" + self.cm_id + " .oe_m2o_menu_item_noreadonly").removeClass("oe-m2o-disabled-cm");
1648                         } else {
1649                             $("#" + self.cm_id + " .oe_m2o_menu_item_noreadonly").addClass("oe-m2o-disabled-cm");
1650                         }
1651                         return true;
1652                     }, menuStyle: {width: "200px"}
1653                 });
1654                 setTimeout(function() {self.$menu_btn.trigger(e);}, 0);
1655             });
1656         });
1657         var ctx_callback = function(e) {init_context_menu_def.resolve(e); e.preventDefault()};
1658         this.$menu_btn.bind('contextmenu', ctx_callback);
1659         this.$menu_btn.click(ctx_callback);
1660
1661         // some behavior for input
1662         this.$input.keyup(function() {
1663             if (self.$input.val() === "") {
1664                 self._change_int_value(null);
1665             } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) {
1666                 self._change_int_value(undefined);
1667             }
1668         });
1669         this.$drop_down.click(function() {
1670             if (self.readonly)
1671                 return;
1672             if (self.$input.autocomplete("widget").is(":visible")) {
1673                 self.$input.autocomplete("close");
1674             } else {
1675                 if (self.value) {
1676                     self.$input.autocomplete("search", "");
1677                 } else {
1678                     self.$input.autocomplete("search");
1679                 }
1680                 self.$input.focus();
1681             }
1682         });
1683         var anyoneLoosesFocus = function() {
1684             if (!self.$input.is(":focus") &&
1685                     !self.$input.autocomplete("widget").is(":visible") &&
1686                     !self.value) {
1687                 if (self.value === undefined && self.last_search.length > 0) {
1688                     self._change_int_ext_value(self.last_search[0]);
1689                 } else {
1690                     self._change_int_ext_value(null);
1691                 }
1692             }
1693         };
1694         this.$input.focusout(anyoneLoosesFocus);
1695
1696         var isSelecting = false;
1697         // autocomplete
1698         this.$input.autocomplete({
1699             source: function(req, resp) { self.get_search_result(req, resp); },
1700             select: function(event, ui) {
1701                 isSelecting = true;
1702                 var item = ui.item;
1703                 if (item.id) {
1704                     self._change_int_value([item.id, item.name]);
1705                 } else if (item.action) {
1706                     self._change_int_value(undefined);
1707                     item.action();
1708                     return false;
1709                 }
1710             },
1711             focus: function(e, ui) {
1712                 e.preventDefault();
1713             },
1714             html: true,
1715             close: anyoneLoosesFocus,
1716             minLength: 0,
1717             delay: 0
1718         });
1719         // used to correct a bug when selecting an element by pushing 'enter' in an editable list
1720         this.$input.keyup(function(e) {
1721             if (e.which === 13) {
1722                 if (isSelecting)
1723                     e.stopPropagation();
1724             }
1725             isSelecting = false;
1726         });
1727     },
1728     // autocomplete component content handling
1729     get_search_result: function(request, response) {
1730         var search_val = request.term;
1731         var self = this;
1732
1733         var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
1734
1735         dataset.name_search(search_val, self.build_domain(), 'ilike',
1736                 this.limit + 1, function(data) {
1737             self.last_search = data;
1738             // possible selections for the m2o
1739             var values = _.map(data, function(x) {
1740                 return {label: $('<span />').text(x[1]).html(), name:x[1], id:x[0]};
1741             });
1742
1743             // search more... if more results that max
1744             if (values.length > self.limit) {
1745                 values = values.slice(0, self.limit);
1746                 values.push({label: _t("<em>   Search More...</em>"), action: function() {
1747                     dataset.name_search(search_val, self.build_domain(), 'ilike'
1748                     , false, function(data) {
1749                         self._change_int_value(null);
1750                         self._search_create_popup("search", data);
1751                     });
1752                 }});
1753             }
1754             // quick create
1755             var raw_result = _(data.result).map(function(x) {return x[1];});
1756             if (search_val.length > 0 &&
1757                 !_.include(raw_result, search_val) &&
1758                 (!self.value || search_val !== self.value[1])) {
1759                 values.push({label: _.sprintf(_t('<em>   Create "<strong>%s</strong>"</em>'),
1760                         $('<span />').text(search_val).html()), action: function() {
1761                     self._quick_create(search_val);
1762                 }});
1763             }
1764             // create...
1765             values.push({label: _t("<em>   Create and Edit...</em>"), action: function() {
1766                 self._change_int_value(null);
1767                 self._search_create_popup("form", undefined, {"default_name": search_val});
1768             }});
1769
1770             response(values);
1771         });
1772     },
1773     _quick_create: function(name) {
1774         var self = this;
1775         var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
1776         dataset.name_create(name, function(data) {
1777             self._change_int_ext_value(data);
1778         }).fail(function(error, event) {
1779             event.preventDefault();
1780             self._change_int_value(null);
1781             self._search_create_popup("form", undefined, {"default_name": name});
1782         });
1783     },
1784     // all search/create popup handling
1785     _search_create_popup: function(view, ids, context) {
1786         var self = this;
1787         var pop = new openerp.web.form.SelectCreatePopup(this);
1788         pop.select_element(self.field.relation,{
1789                 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
1790                 initial_view: view,
1791                 disable_multiple_selection: true
1792                 }, self.build_domain(),
1793                 new openerp.web.CompoundContext(self.build_context(), context || {}));
1794         pop.on_select_elements.add(function(element_ids) {
1795             var dataset = new openerp.web.DataSetStatic(self, self.field.relation, self.build_context());
1796             dataset.name_get([element_ids[0]], function(data) {
1797                 self._change_int_ext_value(data[0]);
1798             });
1799         });
1800     },
1801     _change_int_ext_value: function(value) {
1802         this._change_int_value(value);
1803         this.$input.val(this.value ? this.value[1] : "");
1804     },
1805     _change_int_value: function(value) {
1806         this.value = value;
1807         var back_orig_value = this.original_value;
1808         if (this.value === null || this.value) {
1809             this.original_value = this.value;
1810         }
1811         if (back_orig_value === undefined) { // first use after a set_value()
1812             return;
1813         }
1814         if (this.value !== undefined && ((back_orig_value ? back_orig_value[0] : null)
1815                 !== (this.value ? this.value[0] : null))) {
1816             this.on_ui_change();
1817         }
1818     },
1819     set_value: function(value) {
1820         value = value || null;
1821         this.invalid = false;
1822         var self = this;
1823         this.tmp_value = value;
1824         self.update_dom();
1825         self.on_value_changed();
1826         var real_set_value = function(rval) {
1827             self.tmp_value = undefined;
1828             self.value = rval;
1829             self.original_value = undefined;
1830             self._change_int_ext_value(rval);
1831         };
1832         if(!(typeof(value) instanceof Array)) {
1833             var dataset = new openerp.web.DataSetStatic(this, this.field.relation, self.build_context());
1834             dataset.name_get([value], function(data) {
1835                 real_set_value(data[0]);
1836             }).fail(function() {self.tmp_value = undefined;});
1837         } else {
1838             setTimeout(function() {real_set_value(value);}, 0);
1839         }
1840     },
1841     get_value: function() {
1842         if (this.tmp_value !== undefined) {
1843             if (this.tmp_value instanceof Array) {
1844                 return this.tmp_value[0];
1845             }
1846             return this.tmp_value ? this.tmp_value : false;
1847         }
1848         if (this.value === undefined)
1849             return this.original_value ? this.original_value[0] : false;
1850         return this.value ? this.value[0] : false;
1851     },
1852     validate: function() {
1853         this.invalid = false;
1854         var val = this.tmp_value !== undefined ? this.tmp_value : this.value;
1855         if (val === null) {
1856             this.invalid = this.required;
1857         }
1858     },
1859     open_related: function(related) {
1860         var self = this;
1861         if (!self.value)
1862             return;
1863         var additional_context = {
1864                 active_id: self.value[0],
1865                 active_ids: [self.value[0]],
1866                 active_model: self.field.relation
1867         };
1868         self.rpc("/web/action/load", {
1869             action_id: related[2].id,
1870             context: additional_context
1871         }, function(result) {
1872             result.result.context = _.extend(result.result.context || {}, additional_context);
1873             self.do_action(result.result);
1874         });
1875     },
1876     focus: function () {
1877         this.$input.focus();
1878     },
1879     update_dom: function() {
1880         this._super.apply(this, arguments);
1881         this.$input.attr('disabled', this.readonly);
1882     }
1883 });
1884
1885 /*
1886 # Values: (0, 0,  { fields })    create
1887 #         (1, ID, { fields })    update
1888 #         (2, ID)                remove (delete)
1889 #         (3, ID)                unlink one (target id or target of relation)
1890 #         (4, ID)                link
1891 #         (5)                    unlink all (only valid for one2many)
1892 */
1893 var commands = {
1894     // (0, _, {values})
1895     CREATE: 0,
1896     'create': function (values) {
1897         return [commands.CREATE, false, values];
1898     },
1899     // (1, id, {values})
1900     UPDATE: 1,
1901     'update': function (id, values) {
1902         return [commands.UPDATE, id, values];
1903     },
1904     // (2, id[, _])
1905     DELETE: 2,
1906     'delete': function (id) {
1907         return [commands.DELETE, id, false];
1908     },
1909     // (3, id[, _]) removes relation, but not linked record itself
1910     FORGET: 3,
1911     'forget': function (id) {
1912         return [commands.FORGET, id, false];
1913     },
1914     // (4, id[, _])
1915     LINK_TO: 4,
1916     'link_to': function (id) {
1917         return [commands.LINK_TO, id, false];
1918     },
1919     // (5[, _[, _]])
1920     DELETE_ALL: 5,
1921     'delete_all': function () {
1922         return [5, false, false];
1923     },
1924     // (6, _, ids) replaces all linked records with provided ids
1925     REPLACE_WITH: 6,
1926     'replace_with': function (ids) {
1927         return [6, false, ids];
1928     }
1929 };
1930 openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({
1931     template: 'FieldOne2Many',
1932     multi_selection: false,
1933     init: function(view, node) {
1934         this._super(view, node);
1935         this.is_loaded = $.Deferred();
1936         this.initial_is_loaded = this.is_loaded;
1937         this.is_setted = $.Deferred();
1938         this.form_last_update = $.Deferred();
1939         this.init_form_last_update = this.form_last_update;
1940         this.disable_utility_classes = true;
1941     },
1942     start: function() {
1943         this._super.apply(this, arguments);
1944
1945         var self = this;
1946
1947         this.dataset = new openerp.web.form.One2ManyDataSet(this, this.field.relation);
1948         this.dataset.o2m = this;
1949         this.dataset.parent_view = this.view;
1950         this.dataset.on_change.add_last(function() {
1951             self.on_ui_change();
1952         });
1953
1954         this.is_setted.then(function() {
1955             self.load_views();
1956         });
1957     },
1958     is_readonly: function() {
1959         return this.readonly || this.force_readonly;
1960     },
1961     load_views: function() {
1962         var self = this;
1963         
1964         var modes = this.node.attrs.mode;
1965         modes = !!modes ? modes.split(",") : ["tree"];
1966         var views = [];
1967         _.each(modes, function(mode) {
1968             var view = {
1969                 view_id: false,
1970                 view_type: mode == "tree" ? "list" : mode,
1971                 options: { sidebar : false }
1972             };
1973             if (self.field.views && self.field.views[mode]) {
1974                 view.embedded_view = self.field.views[mode];
1975             }
1976             if(view.view_type === "list") {
1977                 view.options.selectable = self.multi_selection;
1978                 if (self.is_readonly()) {
1979                     view.options.addable = null;
1980                     view.options.deletable = null;
1981                 }
1982             } else if (view.view_type === "form") {
1983                 view.options.not_interactible_on_create = true;
1984             }
1985             views.push(view);
1986         });
1987         this.views = views;
1988         
1989         this.viewmanager = new openerp.web.ViewManager(this, this.dataset, views);
1990         this.viewmanager.registry = openerp.web.views.clone({
1991             list: 'openerp.web.form.One2ManyListView',
1992             form: 'openerp.web.FormView'
1993         });
1994         var once = $.Deferred().then(function() {
1995             self.init_form_last_update.resolve();
1996         });
1997         var def = $.Deferred().then(function() {
1998             self.initial_is_loaded.resolve();
1999         });
2000         this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
2001             if (view_type == "list") {
2002                 controller.o2m = self;
2003                 if (self.is_readonly())
2004                     controller.set_editable(false);
2005             } else if (view_type == "form") {
2006                 if (self.is_readonly()) {
2007                     controller.on_toggle_readonly();
2008                     $(controller.$element.find(".oe_form_buttons")[0]).children().remove();
2009                 }
2010                 controller.on_record_loaded.add_last(function() {
2011                     once.resolve();
2012                 });
2013                 controller.on_pager_action.add_first(function() {
2014                     self.save_form_view();
2015                 });
2016                 controller.$element.find(".oe_form_button_save").hide();
2017             } else if (view_type == "graph") {
2018                 self.reload_current_view()
2019             }
2020             def.resolve();
2021         });
2022         this.viewmanager.on_mode_switch.add_first(function(n_mode, b, c, d, e) {
2023             $.when(self.save_form_view()).then(function() {
2024                 if(n_mode === "list")
2025                     setTimeout(function() {self.reload_current_view();}, 0);
2026             });
2027         });
2028         this.is_setted.then(function() {
2029             setTimeout(function () {
2030                 self.viewmanager.appendTo(self.$element);
2031             }, 0);
2032         });
2033         return def;
2034     },
2035     reload_current_view: function() {
2036         var self = this;
2037         self.is_loaded = self.is_loaded.pipe(function() {
2038             var view = self.viewmanager.views[self.viewmanager.active_view].controller;
2039             if(self.viewmanager.active_view === "list") {
2040                 return view.reload_content();
2041             } else if (self.viewmanager.active_view === "form") {
2042                 if (self.dataset.index === null && self.dataset.ids.length >= 1) {
2043                     self.dataset.index = 0;
2044                 }
2045                 var act = function() {
2046                     return view.do_show();
2047                 }
2048                 self.form_last_update = self.form_last_update.pipe(act, act);
2049                 return self.form_last_update;
2050             } else if (self.viewmanager.active_view === "graph") {
2051                 return view.do_search(self.build_domain(), self.dataset.get_context(), []);
2052             }
2053         });
2054     },
2055     set_value: function(value) {
2056         value = value || [];
2057         var self = this;
2058         this.dataset.reset_ids([]);
2059         if(value.length >= 1 && value[0] instanceof Array) {
2060             var ids = [];
2061             _.each(value, function(command) {
2062                 var obj = {values: command[2]};
2063                 switch (command[0]) {
2064                     case commands.CREATE:
2065                         obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
2066                         obj.defaults = {};
2067                         self.dataset.to_create.push(obj);
2068                         self.dataset.cache.push(_.clone(obj));
2069                         ids.push(obj.id);
2070                         return;
2071                     case commands.UPDATE:
2072                         obj['id'] = command[1];
2073                         self.dataset.to_write.push(obj);
2074                         self.dataset.cache.push(_.clone(obj));
2075                         ids.push(obj.id);
2076                         return;
2077                     case commands.DELETE:
2078                         self.dataset.to_delete.push({id: command[1]});
2079                         return;
2080                     case commands.LINK_TO:
2081                         ids.push(command[1]);
2082                         return;
2083                     case commands.DELETE_ALL:
2084                         self.dataset.delete_all = true;
2085                         return;
2086                 }
2087             });
2088             this._super(ids);
2089             this.dataset.set_ids(ids);
2090         } else if (value.length >= 1 && typeof(value[0]) === "object") {
2091             var ids = [];
2092             this.dataset.delete_all = true;
2093             _.each(value, function(command) {
2094                 var obj = {values: command};
2095                 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
2096                 obj.defaults = {};
2097                 self.dataset.to_create.push(obj);
2098                 self.dataset.cache.push(_.clone(obj));
2099                 ids.push(obj.id);
2100             });
2101             this._super(ids);
2102             this.dataset.set_ids(ids);
2103         } else {
2104             this._super(value);
2105             this.dataset.reset_ids(value);
2106         }
2107         if (this.dataset.index === null && this.dataset.ids.length > 0) {
2108             this.dataset.index = 0;
2109         }
2110         self.reload_current_view();
2111         this.is_setted.resolve();
2112     },
2113     get_value: function() {
2114         var self = this;
2115         if (!this.dataset)
2116             return [];
2117         var val = this.dataset.delete_all ? [commands.delete_all()] : [];
2118         val = val.concat(_.map(this.dataset.ids, function(id) {
2119             var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
2120             if (alter_order) {
2121                 return commands.create(alter_order.values);
2122             }
2123             alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
2124             if (alter_order) {
2125                 return commands.update(alter_order.id, alter_order.values);
2126             }
2127             return commands.link_to(id);
2128         }));
2129         return val.concat(_.map(
2130             this.dataset.to_delete, function(x) {
2131                 return commands['delete'](x.id);}));
2132     },
2133     save_form_view: function() {
2134         if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
2135             this.viewmanager.views[this.viewmanager.active_view] &&
2136             this.viewmanager.views[this.viewmanager.active_view].controller) {
2137             var view = this.viewmanager.views[this.viewmanager.active_view].controller;
2138             if (this.viewmanager.active_view === "form") {
2139                 var res = $.when(view.do_save());
2140                 // it seems line there are some cases when this happens
2141                 /*if (!res.isResolved() && !res.isRejected()) {
2142                     console.warn("Asynchronous get_value() is not supported in form view.");
2143                 }*/
2144                 return res;
2145             }
2146         }
2147         return false;
2148     },
2149     is_valid: function() {
2150         this.validate();
2151         return this._super();
2152     },
2153     validate: function() {
2154         this.invalid = false;
2155         if (!this.viewmanager.views[this.viewmanager.active_view])
2156             return;
2157         var view = this.viewmanager.views[this.viewmanager.active_view].controller;
2158         if (this.viewmanager.active_view === "form") {
2159             for (var f in view.fields) {
2160                 f = view.fields[f];
2161                 if (!f.is_valid()) {
2162                     this.invalid = true;
2163                     return;
2164                 }
2165             }
2166         }
2167     },
2168     is_dirty: function() {
2169         this.save_form_view();
2170         return this._super();
2171     },
2172     update_dom: function() {
2173         this._super.apply(this, arguments);
2174         var self = this;
2175         if (this.previous_readonly !== this.readonly) {
2176             this.previous_readonly = this.readonly;
2177             if (this.viewmanager) {
2178                 this.is_loaded = this.is_loaded.pipe(function() {
2179                     self.viewmanager.stop();
2180                     return $.when(self.load_views()).then(function() {
2181                         self.reload_current_view();
2182                     });
2183                 });
2184             }
2185         }
2186     }
2187 });
2188
2189 openerp.web.form.One2ManyDataSet = openerp.web.BufferedDataSet.extend({
2190     get_context: function() {
2191         this.context = this.o2m.build_context();
2192         return this.context;
2193     }
2194 });
2195
2196 openerp.web.form.One2ManyListView = openerp.web.ListView.extend({
2197     do_add_record: function () {
2198         if (this.options.editable) {
2199             this._super.apply(this, arguments);
2200         } else {
2201             var self = this;
2202             var pop = new openerp.web.form.SelectCreatePopup(this);
2203             pop.on_default_get.add(self.dataset.on_default_get);
2204             pop.select_element(self.o2m.field.relation,{
2205                 initial_view: "form",
2206                 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
2207                 create_function: function(data, callback, error_callback) {
2208                     return self.o2m.dataset.create(data).then(function(r) {
2209                         self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result]));
2210                         self.o2m.dataset.on_change();
2211                     }).then(callback, error_callback);
2212                 },
2213                 read_function: function() {
2214                     return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
2215                 },
2216                 parent_view: self.o2m.view,
2217                 form_view_options: {'not_interactible_on_create':true}
2218             }, self.o2m.build_domain(), self.o2m.build_context());
2219             pop.on_select_elements.add_last(function() {
2220                 self.o2m.reload_current_view();
2221             });
2222         }
2223     },
2224     do_activate_record: function(index, id) {
2225         var self = this;
2226         var pop = new openerp.web.form.FormOpenPopup(self.o2m.view);
2227         pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(),{
2228             auto_write: false,
2229             alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
2230             parent_view: self.o2m.view,
2231             read_function: function() {
2232                 return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
2233             },
2234             form_view_options: {'not_interactible_on_create':true},
2235             readonly: self.o2m.is_readonly()
2236         });
2237         pop.on_write.add(function(id, data) {
2238             self.o2m.dataset.write(id, data, {}, function(r) {
2239                 self.o2m.reload_current_view();
2240             });
2241         });
2242     }
2243 });
2244
2245 openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({
2246     template: 'FieldMany2Many',
2247     multi_selection: false,
2248     init: function(view, node) {
2249         this._super(view, node);
2250         this.list_id = _.uniqueId("many2many");
2251         this.is_loaded = $.Deferred();
2252         this.initial_is_loaded = this.is_loaded;
2253         this.is_setted = $.Deferred();
2254     },
2255     start: function() {
2256         this._super.apply(this, arguments);
2257
2258         var self = this;
2259
2260         this.dataset = new openerp.web.form.Many2ManyDataSet(this, this.field.relation);
2261         this.dataset.m2m = this;
2262         this.dataset.on_unlink.add_last(function(ids) {
2263             self.on_ui_change();
2264         });
2265         
2266         this.is_setted.then(function() {
2267             self.load_view();
2268         });
2269     },
2270     set_value: function(value) {
2271         value = value || [];
2272         if (value.length >= 1 && value[0] instanceof Array) {
2273             value = value[0][2];
2274         }
2275         this._super(value);
2276         this.dataset.set_ids(value);
2277         var self = this;
2278         self.reload_content();
2279         this.is_setted.resolve();
2280     },
2281     get_value: function() {
2282         return [commands.replace_with(this.dataset.ids)];
2283     },
2284     validate: function() {
2285         this.invalid = false;
2286     },
2287     is_readonly: function() {
2288         return this.readonly || this.force_readonly;
2289     },
2290     load_view: function() {
2291         var self = this;
2292         this.list_view = new openerp.web.form.Many2ManyListView(this, this.dataset, false, {
2293                     'addable': self.is_readonly() ? null : 'Add',
2294                     'deletable': self.is_readonly() ? false : true,
2295                     'selectable': self.multi_selection
2296             });
2297         this.list_view.m2m_field = this;
2298         var loaded = $.Deferred();
2299         this.list_view.on_loaded.add_last(function() {
2300             self.initial_is_loaded.resolve();
2301             loaded.resolve();
2302         });
2303         setTimeout(function () {
2304             self.list_view.appendTo($("#" + self.list_id));
2305         }, 0);
2306         return loaded;
2307     },
2308     reload_content: function() {
2309         var self = this;
2310         this.is_loaded = this.is_loaded.pipe(function() {
2311             return self.list_view.reload_content();
2312         });
2313     },
2314     update_dom: function() {
2315         this._super.apply(this, arguments);
2316         var self = this;
2317         if (this.previous_readonly !== this.readonly) {
2318             this.previous_readonly = this.readonly;
2319             if (this.list_view) {
2320                 this.is_loaded = this.is_loaded.pipe(function() {
2321                     self.list_view.stop();
2322                     return $.when(self.load_view()).then(function() {
2323                         self.reload_content();
2324                     });
2325                 });
2326             }
2327         }
2328     }
2329 });
2330
2331 openerp.web.form.Many2ManyDataSet = openerp.web.DataSetStatic.extend({
2332     get_context: function() {
2333         this.context = this.m2m.build_context();
2334         return this.context;
2335     }
2336 });
2337
2338 /**
2339  * @class
2340  * @extends openerp.web.ListView
2341  */
2342 openerp.web.form.Many2ManyListView = openerp.web.ListView.extend(/** @lends openerp.web.form.Many2ManyListView# */{
2343     do_add_record: function () {
2344         var pop = new openerp.web.form.SelectCreatePopup(this);
2345         pop.select_element(this.model, {},
2346             new openerp.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
2347             this.m2m_field.build_context());
2348         var self = this;
2349         pop.on_select_elements.add(function(element_ids) {
2350             _.each(element_ids, function(element_id) {
2351                 if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) {
2352                     self.dataset.set_ids([].concat(self.dataset.ids, [element_id]));
2353                     self.m2m_field.on_ui_change();
2354                     self.reload_content();
2355                 }
2356             });
2357         });
2358     },
2359     do_activate_record: function(index, id) {
2360         var self = this;
2361         var pop = new openerp.web.form.FormOpenPopup(this);
2362         pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {});
2363         pop.on_write_completed.add_last(function() {
2364             self.reload_content();
2365         });
2366     }
2367 });
2368
2369 /**
2370  * @class
2371  * @extends openerp.web.OldWidget
2372  */
2373 openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.SelectCreatePopup# */{
2374     identifier_prefix: "selectcreatepopup",
2375     template: "SelectCreatePopup",
2376     /**
2377      * options:
2378      * - initial_ids
2379      * - initial_view: form or search (default search)
2380      * - disable_multiple_selection
2381      * - alternative_form_view
2382      * - create_function (defaults to a naive saving behavior)
2383      * - parent_view
2384      * - form_view_options
2385      * - list_view_options
2386      * - read_function
2387      */
2388     select_element: function(model, options, domain, context) {
2389         var self = this;
2390         this.model = model;
2391         this.domain = domain || [];
2392         this.context = context || {};
2393         this.options = _.defaults(options || {}, {"initial_view": "search", "create_function": function() {
2394             return self.create_row.apply(self, arguments);
2395         }, read_function: null});
2396         this.initial_ids = this.options.initial_ids;
2397         this.created_elements = [];
2398         openerp.web.form.dialog(this.render(), {close:function() {
2399             self.check_exit();
2400         }});
2401         this.start();
2402     },
2403     start: function() {
2404         this._super();
2405         var self = this;
2406         this.dataset = new openerp.web.ProxyDataSet(this, this.model,
2407             this.context);
2408         this.dataset.create_function = function() {
2409             return self.options.create_function.apply(null, arguments).then(function(r) {
2410                 self.created_elements.push(r.result);
2411             });
2412         };
2413         this.dataset.write_function = function() {
2414             return self.write_row.apply(self, arguments);
2415         };
2416         this.dataset.read_function = this.options.read_function;
2417         this.dataset.parent_view = this.options.parent_view;
2418         this.dataset.on_default_get.add(this.on_default_get);
2419         if (this.options.initial_view == "search") {
2420             this.setup_search_view();
2421         } else { // "form"
2422             this.new_object();
2423         }
2424     },
2425     setup_search_view: function() {
2426         var self = this;
2427         if (this.searchview) {
2428             this.searchview.stop();
2429         }
2430         this.searchview = new openerp.web.SearchView(this,
2431                 this.dataset, false, {
2432                     "selectable": !this.options.disable_multiple_selection,
2433                     "deletable": false
2434                 });
2435         this.searchview.on_search.add(function(domains, contexts, groupbys) {
2436             if (self.initial_ids) {
2437                 self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
2438                     contexts, groupbys);
2439                 self.initial_ids = undefined;
2440             } else {
2441                 self.do_search(domains.concat([self.domain]), contexts, groupbys);
2442             }
2443         });
2444         this.searchview.on_loaded.add_last(function () {
2445             var $buttons = self.searchview.$element.find(".oe_search-view-buttons");
2446             $buttons.append(QWeb.render("SelectCreatePopup.search.buttons"));
2447             var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close");
2448             $cbutton.click(function() {
2449                 self.stop();
2450             });
2451             var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select");
2452             if(self.options.disable_multiple_selection) {
2453                 $sbutton.hide();
2454             }
2455             $sbutton.click(function() {
2456                 self.on_select_elements(self.selected_ids);
2457                 self.stop();
2458             });
2459             self.view_list = new openerp.web.form.SelectCreateListView(self,
2460                     self.dataset, false,
2461                     _.extend({'deletable': false}, self.options.list_view_options || {}));
2462             self.view_list.popup = self;
2463             self.view_list.appendTo($("#" + self.element_id + "_view_list")).pipe(function() {
2464                 self.view_list.do_show();
2465             }).pipe(function() {
2466                 self.searchview.do_search();
2467             });
2468         });
2469         this.searchview.appendTo($("#" + this.element_id + "_search"));
2470     },
2471     do_search: function(domains, contexts, groupbys) {
2472         var self = this;
2473         this.rpc('/web/session/eval_domain_and_context', {
2474             domains: domains || [],
2475             contexts: contexts || [],
2476             group_by_seq: groupbys || []
2477         }, function (results) {
2478             self.view_list.do_search(results.domain, results.context, results.group_by);
2479         });
2480     },
2481     create_row: function() {
2482         var self = this;
2483         var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
2484         wdataset.parent_view = this.options.parent_view;
2485         return wdataset.create.apply(wdataset, arguments);
2486     },
2487     write_row: function() {
2488         var self = this;
2489         var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
2490         wdataset.parent_view = this.options.parent_view;
2491         return wdataset.write.apply(wdataset, arguments);
2492     },
2493     on_select_elements: function(element_ids) {
2494     },
2495     on_click_element: function(ids) {
2496         this.selected_ids = ids || [];
2497         if(this.selected_ids.length > 0) {
2498             this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
2499         } else {
2500             this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
2501         }
2502     },
2503     new_object: function() {
2504         var self = this;
2505         if (this.searchview) {
2506             this.searchview.hide();
2507         }
2508         if (this.view_list) {
2509             this.view_list.$element.hide();
2510         }
2511         this.dataset.index = null;
2512         this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options);
2513         if (this.options.alternative_form_view) {
2514             this.view_form.set_embedded_view(this.options.alternative_form_view);
2515         }
2516         this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form"));
2517         this.view_form.on_loaded.add_last(function() {
2518             var $buttons = self.view_form.$element.find(".oe_form_buttons");
2519             $buttons.html(QWeb.render("SelectCreatePopup.form.buttons", {widget:self}));
2520             var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save-new");
2521             $nbutton.click(function() {
2522                 $.when(self.view_form.do_save()).then(function() {
2523                     self.view_form.reload_lock.then(function() {
2524                         self.view_form.on_button_new();
2525                     });
2526                 });
2527             });
2528             var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save");
2529             $nbutton.click(function() {
2530                 $.when(self.view_form.do_save()).then(function() {
2531                     self.view_form.reload_lock.then(function() {
2532                         self.check_exit();
2533                     });
2534                 });
2535             });
2536             var $cbutton = $buttons.find(".oe_selectcreatepopup-form-close");
2537             $cbutton.click(function() {
2538                 self.check_exit();
2539             });
2540         });
2541         this.view_form.do_show();
2542     },
2543     check_exit: function() {
2544         if (this.created_elements.length > 0) {
2545             this.on_select_elements(this.created_elements);
2546         }
2547         this.stop();
2548     },
2549     on_default_get: function(res) {}
2550 });
2551
2552 openerp.web.form.SelectCreateListView = openerp.web.ListView.extend({
2553     do_add_record: function () {
2554         this.popup.new_object();
2555     },
2556     select_record: function(index) {
2557         this.popup.on_select_elements([this.dataset.ids[index]]);
2558         this.popup.stop();
2559     },
2560     do_select: function(ids, records) {
2561         this._super(ids, records);
2562         this.popup.on_click_element(ids);
2563     }
2564 });
2565
2566 /**
2567  * @class
2568  * @extends openerp.web.OldWidget
2569  */
2570 openerp.web.form.FormOpenPopup = openerp.web.OldWidget.extend(/** @lends openerp.web.form.FormOpenPopup# */{
2571     identifier_prefix: "formopenpopup",
2572     template: "FormOpenPopup",
2573     /**
2574      * options:
2575      * - alternative_form_view
2576      * - auto_write (default true)
2577      * - read_function
2578      * - parent_view
2579      * - form_view_options
2580      * - readonly
2581      */
2582     show_element: function(model, row_id, context, options) {
2583         this.model = model;
2584         this.row_id = row_id;
2585         this.context = context || {};
2586         this.options = _.defaults(options || {}, {"auto_write": true});
2587         jQuery(this.render()).dialog({title: '',
2588                     modal: true,
2589                     width: 960,
2590                     height: 600});
2591         this.start();
2592     },
2593     start: function() {
2594         this._super();
2595         this.dataset = new openerp.web.form.FormOpenDataset(this, this.model, this.context);
2596         this.dataset.fop = this;
2597         this.dataset.ids = [this.row_id];
2598         this.dataset.index = 0;
2599         this.dataset.parent_view = this.options.parent_view;
2600         this.setup_form_view();
2601     },
2602     on_write: function(id, data) {
2603         if (!this.options.auto_write)
2604             return;
2605         var self = this;
2606         var wdataset = new openerp.web.DataSetSearch(this, this.model, this.context, this.domain);
2607         wdataset.parent_view = this.options.parent_view;
2608         wdataset.write(id, data, {}, function(r) {
2609             self.on_write_completed();
2610         });
2611     },
2612     on_write_completed: function() {},
2613     setup_form_view: function() {
2614         var self = this;
2615         this.view_form = new openerp.web.FormView(this, this.dataset, false, self.options.form_view_options);
2616         if (this.options.alternative_form_view) {
2617             this.view_form.set_embedded_view(this.options.alternative_form_view);
2618         }
2619         this.view_form.appendTo(this.$element.find("#" + this.element_id + "_view_form"));
2620         var once = $.Deferred().then(function() {
2621             if (self.options.readonly) {
2622                 self.view_form.on_toggle_readonly();
2623             }
2624         });
2625         this.view_form.on_loaded.add_last(function() {
2626             once.resolve();
2627             var $buttons = self.view_form.$element.find(".oe_form_buttons");
2628             $buttons.html(QWeb.render("FormOpenPopup.form.buttons"));
2629             var $nbutton = $buttons.find(".oe_formopenpopup-form-save");
2630             $nbutton.click(function() {
2631                 self.view_form.do_save().then(function() {
2632                     self.stop();
2633                 });
2634             });
2635             if (self.options.readonly) {
2636                 $nbutton.hide();
2637             }
2638             var $cbutton = $buttons.find(".oe_formopenpopup-form-close");
2639             $cbutton.click(function() {
2640                 self.stop();
2641             });
2642             self.view_form.do_show();
2643         });
2644         this.dataset.on_write.add(this.on_write);
2645     }
2646 });
2647
2648 openerp.web.form.FormOpenDataset = openerp.web.ProxyDataSet.extend({
2649     read_ids: function() {
2650         if (this.fop.options.read_function) {
2651             return this.fop.options.read_function.apply(null, arguments);
2652         } else {
2653             return this._super.apply(this, arguments);
2654         }
2655     }
2656 });
2657
2658 openerp.web.form.FieldReference = openerp.web.form.Field.extend({
2659     template: 'FieldReference',
2660     init: function(view, node) {
2661         this._super(view, node);
2662         this.fields_view = {
2663             fields: {
2664                 selection: {
2665                     selection: view.fields_view.fields[this.name].selection
2666                 },
2667                 m2o: {
2668                     relation: null
2669                 }
2670             }
2671         };
2672         this.get_fields_values = view.get_fields_values;
2673         this.do_onchange = this.on_form_changed = this.on_nop;
2674         this.dataset = this.view.dataset;
2675         this.widgets_counter = 0;
2676         this.view_id = 'reference_' + _.uniqueId();
2677         this.widgets = {};
2678         this.fields = {};
2679         this.selection = new openerp.web.form.FieldSelection(this, { attrs: {
2680             name: 'selection',
2681             widget: 'selection'
2682         }});
2683         this.selection.on_value_changed.add_last(this.on_selection_changed);
2684         this.m2o = new openerp.web.form.FieldMany2One(this, { attrs: {
2685             name: 'm2o',
2686             widget: 'many2one'
2687         }});
2688     },
2689     on_nop: function() {
2690     },
2691     on_selection_changed: function() {
2692         var sel = this.selection.get_value();
2693         this.m2o.field.relation = sel;
2694         this.m2o.set_value(null);
2695         this.m2o.$element.toggle(sel !== false);
2696     },
2697     start: function() {
2698         this._super();
2699         this.selection.start();
2700         this.m2o.start();
2701     },
2702     is_valid: function() {
2703         return this.required === false || typeof(this.get_value()) === 'string';
2704     },
2705     is_dirty: function() {
2706         return this.selection.is_dirty() || this.m2o.is_dirty();
2707     },
2708     set_value: function(value) {
2709         this._super(value);
2710         if (typeof(value) === 'string') {
2711             var vals = value.split(',');
2712             this.selection.set_value(vals[0]);
2713             this.m2o.set_value(parseInt(vals[1], 10));
2714         }
2715     },
2716     get_value: function() {
2717         var model = this.selection.get_value(),
2718             id = this.m2o.get_value();
2719         if (typeof(model) === 'string' && typeof(id) === 'number') {
2720             return model + ',' + id;
2721         } else {
2722             return false;
2723         }
2724     }
2725 });
2726
2727 openerp.web.form.FieldBinary = openerp.web.form.Field.extend({
2728     init: function(view, node) {
2729         this._super(view, node);
2730         this.iframe = this.element_id + '_iframe';
2731         this.binary_value = false;
2732     },
2733     start: function() {
2734         this._super.apply(this, arguments);
2735         this.$element.find('input.oe-binary-file').change(this.on_file_change);
2736         this.$element.find('button.oe-binary-file-save').click(this.on_save_as);
2737         this.$element.find('.oe-binary-file-clear').click(this.on_clear);
2738     },
2739     human_filesize : function(size) {
2740         var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
2741         var i = 0;
2742         while (size >= 1024) {
2743             size /= 1024;
2744             ++i;
2745         }
2746         return size.toFixed(2) + ' ' + units[i];
2747     },
2748     on_file_change: function(e) {
2749         // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper
2750         // http://www.html5rocks.com/tutorials/file/dndfiles/
2751         // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler
2752         window[this.iframe] = this.on_file_uploaded;
2753         if ($(e.target).val() != '') {
2754             this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id);
2755             this.$element.find('form.oe-binary-form').submit();
2756             this.$element.find('.oe-binary-progress').show();
2757             this.$element.find('.oe-binary').hide();
2758         }
2759     },
2760     on_file_uploaded: function(size, name, content_type, file_base64) {
2761         delete(window[this.iframe]);
2762         if (size === false) {
2763             this.do_warn("File Upload", "There was a problem while uploading your file");
2764             // TODO: use openerp web crashmanager
2765             console.warn("Error while uploading file : ", name);
2766         } else {
2767             this.on_file_uploaded_and_valid.apply(this, arguments);
2768             this.on_ui_change();
2769         }
2770         this.$element.find('.oe-binary-progress').hide();
2771         this.$element.find('.oe-binary').show();
2772     },
2773     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
2774     },
2775     on_save_as: function() {
2776         var url = '/web/binary/saveas?session_id=' + this.session.session_id + '&model=' +
2777             this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name +
2778             '&fieldname=' + (this.node.attrs.filename || '') + '&t=' + (new Date().getTime());
2779         window.open(url);
2780     },
2781     on_clear: function() {
2782         if (this.value !== false) {
2783             this.value = false;
2784             this.binary_value = false;
2785             this.on_ui_change();
2786         }
2787         return false;
2788     }
2789 });
2790
2791 openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({
2792     template: 'FieldBinaryFile',
2793     update_dom: function() {
2794         this._super.apply(this, arguments);
2795         this.$element.find('.oe-binary-file-set, .oe-binary-file-clear').toggle(!this.readonly);
2796         this.$element.find('input[type=text]').attr('disabled', this.readonly);
2797     },
2798     set_value: function(value) {
2799         this._super.apply(this, arguments);
2800         var show_value = (value != null && value !== false) ? value : '';
2801         this.$element.find('input').eq(0).val(show_value);
2802     },
2803     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
2804         this.value = file_base64;
2805         this.binary_value = true;
2806         var show_value = this.human_filesize(size);
2807         this.$element.find('input').eq(0).val(show_value);
2808         this.set_filename(name);
2809     },
2810     set_filename: function(value) {
2811         var filename = this.node.attrs.filename;
2812         if (this.view.fields[filename]) {
2813             this.view.fields[filename].set_value(value);
2814             this.view.fields[filename].on_ui_change();
2815         }
2816     },
2817     on_clear: function() {
2818         this._super.apply(this, arguments);
2819         this.$element.find('input').eq(0).val('');
2820         this.set_filename('');
2821     }
2822 });
2823
2824 openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
2825     template: 'FieldBinaryImage',
2826     start: function() {
2827         this._super.apply(this, arguments);
2828         this.$image = this.$element.find('img.oe-binary-image');
2829     },
2830     update_dom: function() {
2831         this._super.apply(this, arguments);
2832         this.$element.find('.oe-binary').toggle(!this.readonly);
2833     },
2834     set_value: function(value) {
2835         this._super.apply(this, arguments);
2836         this.set_image_maxwidth();
2837         var url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' +
2838             this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime());
2839         this.$image.attr('src', url);
2840     },
2841     set_image_maxwidth: function() {
2842         this.$image.css('max-width', this.$element.width());
2843     },
2844     on_file_change: function() {
2845         this.set_image_maxwidth();
2846         this._super.apply(this, arguments);
2847     },
2848     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
2849         this.value = file_base64;
2850         this.binary_value = true;
2851         this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64);
2852     },
2853     on_clear: function() {
2854         this._super.apply(this, arguments);
2855         this.$image.attr('src', '/web/static/src/img/placeholder.png');
2856     }
2857 });
2858
2859 openerp.web.form.FieldStatus = openerp.web.form.Field.extend({
2860     template: "EmptyComponent",
2861     start: function() {
2862         this._super();
2863         this.selected_value = null;
2864
2865         this.render_list();
2866     },
2867     set_value: function(value) {
2868         this._super(value);
2869         this.selected_value = value;
2870
2871         this.render_list();
2872     },
2873     render_list: function() {
2874         var self = this;
2875         var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","),
2876             function(x) { return _.trim(x); });
2877         shown = _.select(shown, function(x) { return x.length > 0; });
2878
2879         if (shown.length == 0) {
2880             this.to_show = this.field.selection;
2881         } else {
2882             this.to_show = _.select(this.field.selection, function(x) {
2883                 return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value;
2884             });
2885         }
2886
2887         var content = openerp.web.qweb.render("FieldStatus.content", {widget: this, _:_});
2888         this.$element.html(content);
2889
2890         var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}");
2891         var color = colors[this.selected_value];
2892         if (color) {
2893             var elem = this.$element.find("li.oe-arrow-list-selected span");
2894             elem.css("border-color", color);
2895             if (this.check_white(color))
2896                 elem.css("color", "white");
2897             elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-before");
2898             elem.css("border-left-color", "rgba(0,0,0,0)");
2899             elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-after");
2900             elem.css("border-color", "rgba(0,0,0,0)");
2901             elem.css("border-left-color", color);
2902         }
2903     },
2904     check_white: function(color) {
2905         var div = $("<div></div>");
2906         div.css("display", "none");
2907         div.css("color", color);
2908         div.appendTo($("body"));
2909         var ncolor = div.css("color");
2910         div.remove();
2911         var res = /^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/.exec(ncolor);
2912         if (!res) {
2913             return false;
2914         }
2915         var comps = [parseInt(res[1]), parseInt(res[2]), parseInt(res[3])];
2916         var lum = comps[0] * 0.3 + comps[1] * 0.59 + comps[1] * 0.11;
2917         if (lum < 128) {
2918             return true;
2919         }
2920         return false;
2921     }
2922 });
2923
2924 openerp.web.form.FieldReadonly = openerp.web.form.Field.extend({
2925
2926 });
2927 openerp.web.form.FieldCharReadonly = openerp.web.form.FieldReadonly.extend({
2928     template: 'FieldChar.readonly',
2929     init: function(view, node) {
2930         this._super(view, node);
2931         this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
2932     },
2933     set_value: function (value) {
2934         this._super.apply(this, arguments);
2935         var show_value = openerp.web.format_value(value, this, '');
2936         if (this.password) {
2937             show_value = new Array(show_value.length + 1).join('*');
2938         }
2939         this.$element.find('div').text(show_value);
2940         return show_value;
2941     }
2942 });
2943 openerp.web.form.FieldURIReadonly = openerp.web.form.FieldCharReadonly.extend({
2944     template: 'FieldURI.readonly',
2945     scheme: null,
2946     set_value: function (value) {
2947         var displayed = this._super.apply(this, arguments);
2948         this.$element.find('a')
2949                 .attr('href', this.scheme + ':' + displayed)
2950                 .text(displayed);
2951     }
2952 });
2953 openerp.web.form.FieldEmailReadonly = openerp.web.form.FieldURIReadonly.extend({
2954     scheme: 'mailto'
2955 });
2956 openerp.web.form.FieldUrlReadonly = openerp.web.form.FieldURIReadonly.extend({
2957     set_value: function (value) {
2958         var s = /(\w+):(.+)/.exec(value);
2959         if (!s || !(s[1] === 'http' || s[1] === 'https')) { return; }
2960         this.scheme = s[1];
2961         this._super(s[2]);
2962     }
2963 });
2964 openerp.web.form.FieldBooleanReadonly = openerp.web.form.FieldCharReadonly.extend({
2965     set_value: function (value) {
2966         this._super(value ? '\u2611' : '\u2610');
2967     }
2968 });
2969 openerp.web.form.FieldSelectionReadonly = openerp.web.form.FieldReadonly.extend({
2970     template: 'FieldChar.readonly',
2971     init: function(view, node) {
2972         // lifted straight from r/w version
2973         var self = this;
2974         this._super(view, node);
2975         this.values = this.field.selection;
2976         _.each(this.values, function(v, i) {
2977             if (v[0] === false && v[1] === '') {
2978                 self.values.splice(i, 1);
2979             }
2980         });
2981         this.values.unshift([false, '']);
2982     },
2983     set_value: function (value) {
2984         value = value === null ? false : value;
2985         value = value instanceof Array ? value[0] : value;
2986         var option = _(this.values)
2987             .detect(function (record) { return record[0] === value; });
2988         this._super(value);
2989         this.$element.find('div').text(option ? option[1] : this.values[0][1]);
2990     }
2991 });
2992 openerp.web.form.FieldMany2OneReadonly = openerp.web.form.FieldCharReadonly.extend({
2993     set_value: function (value) {
2994         value = value || null;
2995         this.invalid = false;
2996         var self = this;
2997         this.value = value;
2998         self.update_dom();
2999         self.on_value_changed();
3000         var real_set_value = function(rval) {
3001             self.$element.find('div').text(rval ? rval[1] : '');
3002         };
3003         if(!(typeof(value) instanceof Array)) {
3004             var dataset = new openerp.web.DataSetStatic(
3005                     this, this.field.relation, self.build_context());
3006             dataset.name_get([value], function(data) {
3007                 real_set_value(data[0]);
3008             }).fail(function() {self.tmp_value = undefined;});
3009         } else {
3010             setTimeout(function() {real_set_value(value);}, 0);
3011         }
3012     }
3013 });
3014
3015 /**
3016  * Registry of form widgets, called by :js:`openerp.web.FormView`
3017  */
3018 openerp.web.form.widgets = new openerp.web.Registry({
3019     'frame' : 'openerp.web.form.WidgetFrame',
3020     'group' : 'openerp.web.form.WidgetFrame',
3021     'notebook' : 'openerp.web.form.WidgetNotebook',
3022     'notebookpage' : 'openerp.web.form.WidgetNotebookPage',
3023     'separator' : 'openerp.web.form.WidgetSeparator',
3024     'label' : 'openerp.web.form.WidgetLabel',
3025     'button' : 'openerp.web.form.WidgetButton',
3026     'char' : 'openerp.web.form.FieldChar',
3027     'email' : 'openerp.web.form.FieldEmail',
3028     'url' : 'openerp.web.form.FieldUrl',
3029     'text' : 'openerp.web.form.FieldText',
3030     'text_wiki' : 'openerp.web.form.FieldText',
3031     'date' : 'openerp.web.form.FieldDate',
3032     'datetime' : 'openerp.web.form.FieldDatetime',
3033     'selection' : 'openerp.web.form.FieldSelection',
3034     'many2one' : 'openerp.web.form.FieldMany2One',
3035     'many2many' : 'openerp.web.form.FieldMany2Many',
3036     'one2many' : 'openerp.web.form.FieldOne2Many',
3037     'one2many_list' : 'openerp.web.form.FieldOne2Many',
3038     'reference' : 'openerp.web.form.FieldReference',
3039     'boolean' : 'openerp.web.form.FieldBoolean',
3040     'float' : 'openerp.web.form.FieldFloat',
3041     'integer': 'openerp.web.form.FieldFloat',
3042     'float_time': 'openerp.web.form.FieldFloat',
3043     'progressbar': 'openerp.web.form.FieldProgressBar',
3044     'image': 'openerp.web.form.FieldBinaryImage',
3045     'binary': 'openerp.web.form.FieldBinaryFile',
3046     'statusbar': 'openerp.web.form.FieldStatus'
3047 });
3048
3049 openerp.web.form.FieldMany2ManyReadonly = openerp.web.form.FieldMany2Many.extend({
3050     force_readonly: true
3051 });
3052 openerp.web.form.FieldOne2ManyReadonly = openerp.web.form.FieldOne2Many.extend({
3053     force_readonly: true
3054 });
3055 openerp.web.form.readonly = openerp.web.form.widgets.clone({
3056     'char': 'openerp.web.form.FieldCharReadonly',
3057     'email': 'openerp.web.form.FieldEmailReadonly',
3058     'url': 'openerp.web.form.FieldUrlReadonly',
3059     'text': 'openerp.web.form.FieldCharReadonly',
3060     'text_wiki' : 'openerp.web.form.FieldCharReadonly',
3061     'date': 'openerp.web.form.FieldCharReadonly',
3062     'datetime': 'openerp.web.form.FieldCharReadonly',
3063     'selection' : 'openerp.web.form.FieldSelectionReadonly',
3064     'many2one': 'openerp.web.form.FieldMany2OneReadonly',
3065     'many2many' : 'openerp.web.form.FieldMany2ManyReadonly',
3066     'one2many' : 'openerp.web.form.FieldOne2ManyReadonly',
3067     'one2many_list' : 'openerp.web.form.FieldOne2ManyReadonly',
3068     'boolean': 'openerp.web.form.FieldBooleanReadonly',
3069     'float': 'openerp.web.form.FieldCharReadonly',
3070     'integer': 'openerp.web.form.FieldCharReadonly',
3071     'float_time': 'openerp.web.form.FieldCharReadonly'
3072 });
3073
3074 };
3075
3076 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: