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