[imp] created first version of one2manydataset and implemented delete
[odoo/odoo.git] / addons / base / static / src / js / form.js
1
2 openerp.base.form = function (openerp) {
3
4 openerp.base.views.add('form', 'openerp.base.FormView');
5 openerp.base.FormView =  openerp.base.View.extend( /** @lends openerp.base.FormView# */{
6     /**
7      * Indicates that this view is not searchable, and thus that no search
8      * view should be displayed (if there is one active).
9      */
10     searchable: false,
11     template: "FormView",
12     /**
13      * @constructs
14      * @param {openerp.base.Session} session the current openerp session
15      * @param {String} element_id this view's root element id
16      * @param {openerp.base.DataSet} dataset the dataset this view will work with
17      * @param {String} view_id the identifier of the OpenERP view object
18      *
19      * @property {openerp.base.Registry} registry=openerp.base.form.widgets widgets registry for this form view instance
20      */
21     init: function(view_manager, session, element_id, dataset, view_id) {
22         this._super(session, element_id);
23         this.view_manager = view_manager || new openerp.base.NullViewManager();
24         this.dataset = dataset;
25         this.model = dataset.model;
26         this.view_id = view_id;
27         this.fields_view = {};
28         this.widgets = {};
29         this.widgets_counter = 0;
30         this.fields = {};
31         this.datarecord = {};
32         this.ready = false;
33         this.show_invalid = true;
34         this.touched = false;
35         this.flags = this.view_manager.flags || {};
36         this.default_focus_field = null;
37         this.default_focus_button = null;
38         this.registry = openerp.base.form.widgets;
39         this.has_been_loaded = $.Deferred();
40     },
41     start: function() {
42         //this.log('Starting FormView '+this.model+this.view_id)
43         if (this.embedded_view) {
44             return $.Deferred().then(this.on_loaded).resolve({fields_view: this.embedded_view});
45         } else {
46             return this.rpc("/base/formview/load", {"model": this.model, "view_id": this.view_id,
47                 toolbar:!!this.flags.sidebar}, this.on_loaded);
48         }
49     },
50     on_loaded: function(data) {
51         var self = this;
52         this.fields_view = data.fields_view;
53         var frame = new (this.registry.get_object('frame'))(this, this.fields_view.arch);
54
55         this.$element.html(QWeb.render(this.template, { 'frame': frame, 'view': this }));
56         _.each(this.widgets, function(w) {
57             w.start();
58         });
59         this.$element.find('div.oe_form_pager button[data-pager-action]').click(function() {
60             var action = $(this).data('pager-action');
61             self.on_pager_action(action);
62         });
63
64         this.$element.find('#' + this.element_id + '_header button.oe_form_button_save').click(this.do_save);
65         this.$element.find('#' + this.element_id + '_header button.oe_form_button_save_edit').click(this.do_save_edit);
66         this.$element.find('#' + this.element_id + '_header button.oe_form_button_cancel').click(this.do_cancel);
67         this.$element.find('#' + this.element_id + '_header button.oe_form_button_new').click(this.on_button_new);
68
69         this.view_manager.sidebar.set_toolbar(data.fields_view.toolbar);
70         this.has_been_loaded.resolve();
71     },
72     do_show: function () {
73         var self = this;
74         self.$element.show();
75         if (this.dataset.index === null || (this.dataset.index === 0 && this.dataset.ids.length == 0)) {
76             // null index means we should start a new record
77             // 0 index with empty ids means we called the form with empty dataset (wizards, switched to form from empty list, ...)
78             this.on_button_new();
79         } else {
80             this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
81         }
82         this.view_manager.sidebar.do_refresh(true);
83     },
84     do_hide: function () {
85         this.$element.hide();
86     },
87     on_record_loaded: function(record) {
88         this.touched = false;
89         if (record) {
90             this.datarecord = record;
91             for (var f in this.fields) {
92                 var field = this.fields[f];
93                 field.touched = false;
94                 field.set_value(this.datarecord[f] || false);
95                 field.validate();
96             }
97             if (!record.id) {
98                 // New record: Second pass in order to trigger the onchanges
99                 this.touched = true;
100                 this.show_invalid = false;
101                 for (var f in record) {
102                     this.on_form_changed(this.fields[f]);
103                 }
104             }
105             this.on_form_changed();
106             this.show_invalid = this.ready = true;
107         } else {
108             this.log("No record received");
109         }
110         this.do_update_pager(record.id == null);
111         this.do_update_sidebar();
112         if (this.default_focus_field) {
113             this.default_focus_field.focus();
114         }
115     },
116     on_form_changed: function(widget) {
117         if (widget && widget.node.attrs.on_change) {
118             this.do_onchange(widget);
119         } else {
120             for (var w in this.widgets) {
121                 w = this.widgets[w];
122                 w.process_attrs();
123                 w.update_dom();
124             }
125         }
126     },
127     on_pager_action: function(action) {
128         switch (action) {
129             case 'first':
130                 this.dataset.index = 0;
131                 break;
132             case 'previous':
133                 this.dataset.previous();
134                 break;
135             case 'next':
136                 this.dataset.next();
137                 break;
138             case 'last':
139                 this.dataset.index = this.dataset.ids.length - 1;
140                 break;
141         }
142         this.reload();
143     },
144     do_update_pager: function(hide_index) {
145         var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager');
146         var index = hide_index ? '-' : this.dataset.index + 1;
147         $pager.find('span.oe_pager_index').html(index);
148         $pager.find('span.oe_pager_count').html(this.dataset.count);
149     },
150     do_onchange: function(widget, processed) {
151         processed = processed || [];
152         if (widget.node.attrs.on_change) {
153             var self = this;
154             this.ready = false;
155             var onchange = _.trim(widget.node.attrs.on_change);
156             var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
157             if (call) {
158                 var method = call[1], args = [];
159                 var argument_replacement = {
160                     'False' : false,
161                     'True' : true,
162                     'None' : null
163                 }
164                 _.each(call[2].split(','), function(a) {
165                     var field = _.trim(a);
166                     if (field in argument_replacement) {
167                         args.push(argument_replacement[field]);
168                     } else if (self.fields[field]) {
169                         var value = self.fields[field].get_value();
170                         args.push(value == null ? false : value);
171                     } else {
172                         args.push(false);
173                         self.log("warning : on_change can't find field " + field, onchange);
174                     }
175                 });
176                 var ajax = {
177                     url: '/base/dataset/call',
178                     async: false
179                 };
180                 return this.rpc(ajax, {
181                     model: this.dataset.model,
182                     method: method,
183                     args: [(this.datarecord.id == null ? [] : [this.datarecord.id])].concat(args)
184                 }, function(response) {
185                     self.on_processed_onchange(response, processed);
186                 });
187             } else {
188                 this.log("Wrong on_change format", on_change);
189             }
190         }
191     },
192     on_processed_onchange: function(response, processed) {
193         var result = response.result;
194         if (result.value) {
195             for (var f in result.value) {
196                 var field = this.fields[f];
197                 if (field) {
198                     var value = result.value[f];
199                     processed.push(field.name);
200                     if (field.get_value() != value) {
201                         field.set_value(value);
202                         if (_.indexOf(processed, field.name) < 0) {
203                             this.do_onchange(field, processed);
204                         }
205                     }
206                 } else {
207                     this.log("warning : on_processed_onchange can't find field " + field, result);
208                 }
209             }
210             this.on_form_changed();
211         }
212         if (result.warning) {
213             $(QWeb.render("DialogWarning", result.warning)).dialog({
214                 modal: true,
215                 buttons: {
216                     Ok: function() {
217                         $(this).dialog("close");
218                     }
219                 }
220             });
221         }
222         if (result.domain) {
223             // Will be removed ?
224         }
225         this.ready = true;
226     },
227     on_button_new: function() {
228         var self = this;
229         $.when(this.has_been_loaded).then(function() {
230             self.dataset.default_get(_.keys(self.fields_view.fields), function(result) {
231                 self.on_record_loaded(result.result);
232             });
233         });
234     },
235     /**
236      * Triggers saving the form's record. Chooses between creating a new
237      * record or saving an existing one depending on whether the record
238      * already has an id property.
239      *
240      * @param {Function} success callback on save success
241      * @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)
242      */
243     do_save: function(success, prepend_on_create) {
244         var self = this;
245         if (!this.ready) {
246             return false;
247         }
248         var invalid = false;
249         var values = {};
250         for (var f in this.fields) {
251             f = this.fields[f];
252             if (f.invalid) {
253                 invalid = true;
254                 f.update_dom();
255             } else if (f.touched) {
256                 values[f.name] = f.get_value();
257             }
258         }
259         if (invalid) {
260             this.on_invalid();
261             return false;
262         } else {
263             this.log("About to save", values);
264             if (!this.datarecord.id) {
265                 this.dataset.create(values, function(r) {
266                     self.on_created(r, success, prepend_on_create);
267                 });
268             } else {
269                 this.dataset.write(this.datarecord.id, values, function(r) {
270                     self.on_saved(r, success);
271                 });
272             }
273             return true;
274         }
275     },
276     do_save_edit: function() {
277         this.do_save();
278         //this.switch_readonly(); Use promises
279     },
280     switch_readonly: function() {
281     },
282     switch_editable: function() {
283     },
284     on_invalid: function() {
285         var msg = "<ul>";
286         _.each(this.fields, function(f) {
287             if (f.invalid) {
288                 msg += "<li>" + f.string + "</li>";
289             }
290         });
291         msg += "</ul>";
292         this.notification.warn("The following fields are invalid :", msg);
293     },
294     on_saved: function(r, success) {
295         if (!r.result) {
296             this.notification.warn("Record not saved", "Problem while saving record.");
297         } else {
298             this.notification.notify("Record saved", "The record #" + this.datarecord.id + " has been saved.");
299             if (success) {
300                 success(r);
301             }
302         }
303     },
304     /**
305      * Updates the form' dataset to contain the new record:
306      *
307      * * Adds the newly created record to the current dataset (at the end by
308      *   default)
309      * * Selects that record (sets the dataset's index to point to the new
310      *   record's id).
311      * * Updates the pager and sidebar displays
312      *
313      * @param {Object} r
314      * @param {Function} success callback to execute after having updated the dataset
315      * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end
316      */
317     on_created: function(r, success, prepend_on_create) {
318         if (!r.result) {
319             this.notification.warn("Record not created", "Problem while creating record.");
320         } else {
321             this.datarecord.id = r.result;
322             if (!prepend_on_create) {
323                 this.dataset.ids.push(this.datarecord.id);
324                 this.dataset.index = this.dataset.ids.length - 1;
325             } else {
326                 this.dataset.ids.unshift(this.datarecord.id);
327                 this.dataset.index = 0;
328             }
329             this.dataset.count++;
330             this.do_update_pager();
331             this.do_update_sidebar();
332             this.notification.notify("Record created", "The record has been created with id #" + this.datarecord.id);
333             if (success) {
334                 success(_.extend(r, {created: true}));
335             }
336         }
337     },
338     do_search: function (domains, contexts, groupbys) {
339         this.notification.notify("Searching form");
340     },
341     on_action: function (action) {
342         this.notification.notify('Executing action ' + action);
343     },
344     do_cancel: function () {
345         this.notification.notify("Cancelling form");
346     },
347     do_update_sidebar: function() {
348         if (this.flags.sidebar === false) {
349             return;
350         }
351         if (!this.datarecord.id) {
352             this.on_attachments_loaded([]);
353         } else {
354             this.rpc('/base/dataset/search_read', {
355                 model: 'ir.attachment',
356                 fields: ['name', 'url', 'type'],
357                 domain: [['res_model', '=', this.dataset.model], ['res_id', '=', this.datarecord.id], ['type', 'in', ['binary', 'url']]],
358                 context: this.dataset.context
359             }, this.on_attachments_loaded);
360         }
361     },
362     on_attachments_loaded: function(attachments) {
363         this.$sidebar = this.view_manager.sidebar.$element.find('.sidebar-attachments');
364         this.attachments = attachments;
365         this.$sidebar.html(QWeb.render('FormView.sidebar.attachments', this));
366         this.$sidebar.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete);
367         this.$sidebar.find('.oe-binary-file').change(this.on_attachment_changed);
368     },
369     on_attachment_changed: function(e) {
370         window[this.element_id + '_iframe'] = this.do_update_sidebar;
371         var $e = $(e.target);
372         if ($e.val() != '') {
373             this.$sidebar.find('form.oe-binary-form').submit();
374             $e.parent().find('input[type=file]').attr('disabled', 'true');
375             $e.parent().find('button').attr('disabled', 'true').find('img, span').toggle();
376         }
377     },
378     on_attachment_delete: function(e) {
379         var self = this, $e = $(e.currentTarget);
380         var name = _.trim($e.parent().find('a.oe-sidebar-attachments-link').text());
381         if (confirm("Do you really want to delete the attachment " + name + " ?")) {
382             this.rpc('/base/dataset/unlink', {
383                 model: 'ir.attachment',
384                 ids: [parseInt($e.attr('data-id'))]
385             }, function(r) {
386                 $e.parent().remove();
387                 self.notification.notify("Delete an attachment", "The attachment '" + name + "' has been deleted");
388             });
389         }
390     },
391     reload: function() {
392         if (this.datarecord.id) {
393             this.dataset.read_index(_.keys(this.fields_view.fields), this.on_record_loaded);
394         } else {
395             this.on_button_new();
396         }
397     }
398 });
399
400 /** @namespace */
401 openerp.base.form = {};
402
403 openerp.base.form.compute_domain = function(expr, fields) {
404     var stack = [];
405     for (var i = expr.length - 1; i >= 0; i--) {
406         var ex = expr[i];
407         if (ex.length == 1) {
408             var top = stack.pop();
409             switch (ex) {
410                 case '|':
411                     stack.push(stack.pop() || top);
412                     continue;
413                 case '&':
414                     stack.push(stack.pop() && top);
415                     continue;
416                 case '!':
417                     stack.push(!top);
418                     continue;
419                 default:
420                     throw new Error('Unknown domain operator ' + ex);
421             }
422         }
423
424         var field = fields[ex[0]].value;
425         var op = ex[1];
426         var val = ex[2];
427
428         switch (op.toLowerCase()) {
429             case '=':
430             case '==':
431                 stack.push(field == val);
432                 break;
433             case '!=':
434             case '<>':
435                 stack.push(field != val);
436                 break;
437             case '<':
438                 stack.push(field < val);
439                 break;
440             case '>':
441                 stack.push(field > val);
442                 break;
443             case '<=':
444                 stack.push(field <= val);
445                 break;
446             case '>=':
447                 stack.push(field >= val);
448                 break;
449             case 'in':
450                 stack.push(_(val).contains(field));
451                 break;
452             case 'not in':
453                 stack.push(!_(val).contains(field));
454                 break;
455             default:
456                 this.log("Unsupported operator in attrs :", op);
457         }
458     }
459     return _.all(stack);
460 };
461
462 openerp.base.form.Widget = openerp.base.Controller.extend({
463     template: 'Widget',
464     init: function(view, node) {
465         this.view = view;
466         this.node = node;
467         this.attrs = JSON.parse(this.node.attrs.attrs || '{}');
468         this.type = this.type || node.tag;
469         this.element_name = this.element_name || this.type;
470         this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_");
471
472         this._super(this.view.session, this.element_id);
473
474         this.view.widgets[this.element_id] = this;
475         this.children = node.children;
476         this.colspan = parseInt(node.attrs.colspan || 1);
477
478         this.string = this.string || node.attrs.string;
479         this.help = this.help || node.attrs.help;
480         this.invisible = (node.attrs.invisible == '1');
481     },
482     start: function() {
483         this.$element = $('#' + this.element_id);
484     },
485     process_attrs: function() {
486         var compute_domain = openerp.base.form.compute_domain;
487         for (var a in this.attrs) {
488             this[a] = compute_domain(this.attrs[a], this.view.fields);
489         }
490     },
491     update_dom: function() {
492         this.$element.toggle(!this.invisible);
493     },
494     render: function() {
495         var template = this.template;
496         return QWeb.render(template, { "widget": this });
497     }
498 });
499
500 openerp.base.form.WidgetFrame = openerp.base.form.Widget.extend({
501     template: 'WidgetFrame',
502     init: function(view, node) {
503         this._super(view, node);
504         this.columns = node.attrs.col || 4;
505         this.x = 0;
506         this.y = 0;
507         this.table = [];
508         this.add_row();
509         for (var i = 0; i < node.children.length; i++) {
510             var n = node.children[i];
511             if (n.tag == "newline") {
512                 this.add_row();
513             } else {
514                 this.handle_node(n);
515             }
516         }
517         this.set_row_cells_with(this.table[this.table.length - 1]);
518     },
519     add_row: function(){
520         if (this.table.length) {
521             this.set_row_cells_with(this.table[this.table.length - 1]);
522         }
523         var row = [];
524         this.table.push(row);
525         this.x = 0;
526         this.y += 1;
527         return row;
528     },
529     set_row_cells_with: function(row) {
530         for (var i = 0; i < row.length; i++) {
531             var w = row[i];
532             if (w.is_field_label) {
533                 w.width = "1%";
534                 if (row[i + 1]) {
535                     row[i + 1].width = Math.round((100 / this.columns) * (w.colspan + 1) - 1) + '%';
536                 }
537             } else if (w.width === undefined) {
538                 w.width = Math.round((100 / this.columns) * w.colspan) + '%';
539             }
540         }
541     },
542     handle_node: function(node) {
543         var type = this.view.fields_view.fields[node.attrs.name] || {};
544         var widget_type = node.attrs.widget || type.type || node.tag;
545         var widget = new (this.view.registry.get_object(widget_type)) (this.view, node);
546         if (node.tag == 'field') {
547             if (node.attrs.default_focus == '1') {
548                 this.view.default_focus_field = widget;
549             }
550             if (node.attrs.nolabel != '1') {
551                 var label = new (this.view.registry.get_object('label')) (this.view, node);
552                 label["for"] = widget;
553                 this.add_widget(label);
554             }
555         }
556         this.add_widget(widget);
557     },
558     add_widget: function(widget) {
559         var current_row = this.table[this.table.length - 1];
560         if (current_row.length && (this.x + widget.colspan) > this.columns) {
561             current_row = this.add_row();
562         }
563         current_row.push(widget);
564         this.x += widget.colspan;
565         return widget;
566     }
567 });
568
569 openerp.base.form.WidgetNotebook = openerp.base.form.Widget.extend({
570     init: function(view, node) {
571         this._super(view, node);
572         this.template = "WidgetNotebook";
573         this.pages = [];
574         for (var i = 0; i < node.children.length; i++) {
575             var n = node.children[i];
576             if (n.tag == "page") {
577                 var page = new openerp.base.form.WidgetFrame(this.view, n);
578                 this.pages.push(page);
579             }
580         }
581     },
582     start: function() {
583         this._super.apply(this, arguments);
584         this.$element.tabs();
585     }
586 });
587
588 openerp.base.form.WidgetSeparator = openerp.base.form.Widget.extend({
589     init: function(view, node) {
590         this._super(view, node);
591         this.template = "WidgetSeparator";
592     }
593 });
594
595 openerp.base.form.WidgetButton = openerp.base.form.Widget.extend({
596     init: function(view, node) {
597         this._super(view, node);
598         this.template = "WidgetButton";
599         if (node.attrs.default_focus == '1') {
600             // TODO fme: provide enter key binding to widgets
601             this.view.default_focus_button = this;
602         }
603     },
604     start: function() {
605         this._super.apply(this, arguments);
606         this.$element.click(this.on_click);
607     },
608     on_click: function(saved) {
609         var self = this;
610         if (!this.node.attrs.special && this.view.touched && saved !== true) {
611             this.view.do_save(function() {
612                 self.on_click(true);
613             });
614         } else {
615             if (this.node.attrs.confirm) {
616                 var dialog = $('<div>' + this.node.attrs.confirm + '</div>').dialog({
617                     title: 'Confirm',
618                     modal: true,
619                     buttons: {
620                         Ok: function() {
621                             self.on_confirmed();
622                             $(this).dialog("close");
623                         },
624                         Cancel: function() {
625                             $(this).dialog("close");
626                         }
627                     }
628                 });
629             } else {
630                 this.on_confirmed();
631             }
632         }
633     },
634     on_confirmed: function() {
635         var self = this;
636
637         this.view.execute_action(
638             this.node.attrs, this.view.dataset, this.session.action_manager,
639             this.view.datarecord.id, function (result) {
640                 self.log("Button returned", result);
641                 self.view.reload();
642             });
643     }
644 });
645
646 openerp.base.form.WidgetLabel = openerp.base.form.Widget.extend({
647     init: function(view, node) {
648         this.is_field_label = true;
649         this.element_name = 'label_' + node.attrs.name;
650
651         this._super(view, node);
652
653         this.template = "WidgetLabel";
654         this.colspan = 1;
655     },
656     render: function () {
657         if (this['for'] && this.type !== 'label') {
658             return QWeb.render(this.template, {widget: this['for']});
659         }
660         // Actual label widgets should not have a false and have type label
661         return QWeb.render(this.template, {widget: this});
662     }
663 });
664
665 openerp.base.form.Field = openerp.base.form.Widget.extend({
666     init: function(view, node) {
667         this.name = node.attrs.name;
668         this.value = undefined;
669         view.fields[this.name] = this;
670         this.type = node.attrs.widget || view.fields_view.fields[node.attrs.name].type;
671         this.element_name = "field_" + this.name + "_" + this.type;
672
673         this._super(view, node);
674
675         if (node.attrs.nolabel != '1' && this.colspan > 1) {
676             this.colspan--;
677         }
678         this.field = view.fields_view.fields[node.attrs.name] || {};
679         this.string = node.attrs.string || this.field.string;
680         this.help = node.attrs.help || this.field.help;
681         this.invisible = (this.invisible || this.field.invisible == '1');
682         this.nolabel = (this.field.nolabel || node.attrs.nolabel) == '1';
683         this.readonly = (this.field.readonly || node.attrs.readonly) == '1';
684         this.required = (this.field.required || node.attrs.required) == '1';
685         this.invalid = false;
686         this.touched = false;
687     },
688     set_value: function(value) {
689         this.value = value;
690         this.invalid = false;
691         this.update_dom();
692     },
693     set_value_from_ui: function() {
694         this.value = undefined;
695     },
696     get_value: function() {
697         return this.value;
698     },
699     update_dom: function() {
700         this._super.apply(this, arguments);
701         this.$element.toggleClass('disabled', this.readonly);
702         this.$element.toggleClass('required', this.required);
703         if (this.view.show_invalid) {
704             this.$element.toggleClass('invalid', this.invalid);
705         }
706     },
707     on_ui_change: function() {
708         this.touched = this.view.touched = true;
709         this.validate();
710         if (!this.invalid) {
711             this.set_value_from_ui();
712             this.view.on_form_changed(this);
713         } else {
714             this.update_dom();
715         }
716     },
717     validate: function() {
718         this.invalid = false;
719     },
720     focus: function() {
721     }
722 });
723
724 openerp.base.form.FieldChar = openerp.base.form.Field.extend({
725     init: function(view, node) {
726         this._super(view, node);
727         this.template = "FieldChar";
728     },
729     start: function() {
730         this._super.apply(this, arguments);
731         this.$element.find('input').change(this.on_ui_change);
732     },
733     set_value: function(value) {
734         this._super.apply(this, arguments);
735         var show_value = (value != null && value !== false) ? value : '';
736         this.$element.find('input').val(show_value);
737     },
738     update_dom: function() {
739         this._super.apply(this, arguments);
740         this.$element.find('input').attr('disabled', this.readonly);
741     },
742     set_value_from_ui: function() {
743         this.value = this.$element.find('input').val();
744     },
745     validate: function() {
746         this.invalid = false;
747         var value = this.$element.find('input').val();
748         if (value === "") {
749             this.invalid = this.required;
750         } else if (this.validation_regex) {
751             this.invalid = !this.validation_regex.test(value);
752         }
753     },
754     focus: function() {
755         this.$element.find('input').focus();
756     }
757 });
758
759 openerp.base.form.FieldEmail = openerp.base.form.FieldChar.extend({
760     init: function(view, node) {
761         this._super(view, node);
762         this.template = "FieldEmail";
763         this.validation_regex = /@/;
764     },
765     start: function() {
766         this._super.apply(this, arguments);
767         this.$element.find('button').click(this.on_button_clicked);
768     },
769     on_button_clicked: function() {
770         if (!this.value || this.invalid) {
771             this.notification.warn("E-mail error", "Can't send email to invalid e-mail address");
772         } else {
773             location.href = 'mailto:' + this.value;
774         }
775     },
776     set_value: function(value) {
777         this._super.apply(this, arguments);
778         var show_value = (value != null && value !== false) ? value : '';
779         this.$element.find('a').attr('href', 'mailto:' + show_value);
780     }
781 });
782
783 openerp.base.form.FieldUrl = openerp.base.form.FieldChar.extend({
784     init: function(view, node) {
785         this._super(view, node);
786         this.template = "FieldUrl";
787     },
788     start: function() {
789         this._super.apply(this, arguments);
790         this.$element.find('button').click(this.on_button_clicked);
791     },
792     on_button_clicked: function() {
793         if (!this.value) {
794             this.notification.warn("Resource error", "This resource is empty");
795         } else {
796             window.open(this.value);
797         }
798     }
799 });
800
801 openerp.base.form.FieldFloat = openerp.base.form.FieldChar.extend({
802     init: function(view, node) {
803         this._super(view, node);
804         this.validation_regex = /^-?\d+(\.\d+)?$/;
805     },
806     set_value: function(value) {
807         this._super.apply(this, [value]);
808         if (value === false || value === undefined) {
809             // As in GTK client, floats default to 0
810             value = 0;
811         }
812         var show_value = value.toFixed(2);
813         this.$element.find('input').val(show_value);
814     },
815     set_value_from_ui: function() {
816         this.value = Number(this.$element.find('input').val().replace(/,/g, '.'));
817     }
818 });
819
820 openerp.base.form.FieldDatetime = openerp.base.form.Field.extend({
821     init: function(view, node) {
822         this._super(view, node);
823         this.template = "FieldDate";
824         this.jqueryui_object = 'datetimepicker';
825     },
826     start: function() {
827         this._super.apply(this, arguments);
828         this.$element.find('input').change(this.on_ui_change)[this.jqueryui_object]({
829             dateFormat: 'yy-mm-dd',
830             timeFormat: 'hh:mm:ss'
831         });
832     },
833     set_value: function(value) {
834         this._super.apply(this, arguments);
835         if (value == null || value == false) {
836             this.$element.find('input').val('');
837         } else {
838             this.$element.find('input').unbind('change');
839             // jQuery UI date picker wrongly call on_change event herebelow
840             this.$element.find('input')[this.jqueryui_object]('setDate', this.parse(value));
841             this.$element.find('input').change(this.on_ui_change);
842         }
843     },
844     set_value_from_ui: function() {
845         this.value = this.$element.find('input')[this.jqueryui_object]('getDate') || false;
846         if (this.value) {
847             this.value = this.format(this.value);
848         }
849     },
850     validate: function() {
851         this.invalid = this.required && this.$element.find('input')[this.jqueryui_object]('getDate') === '';
852     },
853     focus: function() {
854         this.$element.find('input').focus();
855     },
856     parse: openerp.base.parse_datetime,
857     format: openerp.base.format_datetime
858 });
859
860 openerp.base.form.FieldDate = openerp.base.form.FieldDatetime.extend({
861     init: function(view, node) {
862         this._super(view, node);
863         this.jqueryui_object = 'datepicker';
864     },
865     parse: openerp.base.parse_date,
866     format: openerp.base.format_date
867 });
868
869 openerp.base.form.FieldFloatTime = openerp.base.form.FieldChar.extend({
870     init: function(view, node) {
871         this._super(view, node);
872         this.validation_regex = /^\d+:\d+$/;
873     },
874     set_value: function(value) {
875         this._super.apply(this, [value]);
876         if (value === false || value === undefined) {
877             // As in GTK client, floats default to 0
878             value = 0;
879         }
880         var show_value = _.sprintf("%02d:%02d", Math.floor(value), Math.round((value % 1) * 60));
881         this.$element.find('input').val(show_value);
882     },
883     set_value_from_ui: function() {
884         var time = this.$element.find('input').val().split(':');
885         this.set_value(parseInt(time[0], 10) + parseInt(time[1], 10) / 60);
886     }
887 });
888
889 openerp.base.form.FieldText = openerp.base.form.Field.extend({
890     init: function(view, node) {
891         this._super(view, node);
892         this.template = "FieldText";
893         this.validation_regex = null;
894     },
895     start: function() {
896         this._super.apply(this, arguments);
897         this.$element.find('textarea').change(this.on_ui_change);
898     },
899     set_value: function(value) {
900         this._super.apply(this, arguments);
901         var show_value = (value != null && value !== false) ? value : '';
902         this.$element.find('textarea').val(show_value);
903     },
904     update_dom: function() {
905         this._super.apply(this, arguments);
906         this.$element.find('textarea').attr('disabled', this.readonly);
907     },
908     set_value_from_ui: function() {
909         this.value = this.$element.find('textarea').val();
910     },
911     validate: function() {
912         this.invalid = false;
913         var value = this.$element.find('textarea').val();
914         if (value === "") {
915             this.invalid = this.required;
916         } else if (this.validation_regex) {
917             this.invalid = !this.validation_regex.test(value);
918         }
919     },
920     focus: function() {
921         this.$element.find('textarea').focus();
922     }
923 });
924
925 openerp.base.form.FieldBoolean = openerp.base.form.Field.extend({
926     init: function(view, node) {
927         this._super(view, node);
928         this.template = "FieldBoolean";
929     },
930     start: function() {
931         var self = this;
932         this._super.apply(this, arguments);
933         this.$element.find('input').click(function() {
934             if ($(this).is(':checked') != self.value) {
935                 self.on_ui_change();
936             }
937         });
938     },
939     set_value: function(value) {
940         this._super.apply(this, arguments);
941         this.$element.find('input')[0].checked = value;
942     },
943     set_value_from_ui: function() {
944         this.value = this.$element.find('input').is(':checked');
945     },
946     update_dom: function() {
947         this._super.apply(this, arguments);
948         this.$element.find('input').attr('disabled', this.readonly);
949     },
950     validate: function() {
951         this.invalid = this.required && !this.$element.find('input').is(':checked');
952     },
953     focus: function() {
954         this.$element.find('input').focus();
955     }
956 });
957
958 openerp.base.form.FieldProgressBar = openerp.base.form.Field.extend({
959     init: function(view, node) {
960         this._super(view, node);
961         this.template = "FieldProgressBar";
962     },
963     start: function() {
964         this._super.apply(this, arguments);
965         this.$element.find('div').progressbar({
966             value: this.value,
967             disabled: this.readonly
968         });
969     },
970     set_value: function(value) {
971         this._super.apply(this, arguments);
972         var show_value = Number(value);
973         if (show_value === NaN) {
974             show_value = 0;
975         }
976         this.$element.find('div').progressbar('option', 'value', show_value).find('span').html(show_value + '%');
977     }
978 });
979
980 openerp.base.form.FieldTextXml = openerp.base.form.Field.extend({
981 // to replace view editor
982 });
983
984 openerp.base.form.FieldSelection = openerp.base.form.Field.extend({
985     init: function(view, node) {
986         this._super(view, node);
987         this.template = "FieldSelection";
988     },
989     start: function() {
990         this._super.apply(this, arguments);
991         this.$element.find('select').change(this.on_ui_change);
992     },
993     set_value: function(value) {
994         this._super.apply(this, arguments);
995         if (value != null && value !== false) {
996             this.$element.find('select').val(value);
997         } else {
998             this.$element.find('select')[0].selectedIndex = 0;
999         }
1000     },
1001     set_value_from_ui: function() {
1002         this.value = this.$element.find('select').val();
1003     },
1004     update_dom: function() {
1005         this._super.apply(this, arguments);
1006         this.$element.find('select').attr('disabled', this.readonly);
1007     },
1008     validate: function() {
1009         this.invalid = this.required && this.$element.find('select').val() === "";
1010     },
1011     focus: function() {
1012         this.$element.find('select').focus();
1013     }
1014 });
1015
1016 // jquery autocomplete tweak to allow html
1017 (function() {
1018     var proto = $.ui.autocomplete.prototype,
1019         initSource = proto._initSource;
1020
1021     function filter( array, term ) {
1022         var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
1023         return $.grep( array, function(value) {
1024             return matcher.test( $( "<div>" ).html( value.label || value.value || value ).text() );
1025         });
1026     }
1027
1028     $.extend( proto, {
1029         _initSource: function() {
1030             if ( this.options.html && $.isArray(this.options.source) ) {
1031                 this.source = function( request, response ) {
1032                     response( filter( this.options.source, request.term ) );
1033                 };
1034             } else {
1035                 initSource.call( this );
1036             }
1037         },
1038
1039         _renderItem: function( ul, item) {
1040             return $( "<li></li>" )
1041                 .data( "item.autocomplete", item )
1042                 .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
1043                 .appendTo( ul );
1044         }
1045     });
1046 })();
1047
1048 /**
1049  * Builds a new context usable for operations related to fields by merging
1050  * the fields'context with the action's context.
1051  */
1052 var build_relation_context = function(relation_field) {
1053     var action = relation_field.view.view_manager.action || {};
1054     var a_context = action.context || {};
1055     var f_context = relation_field.field.context || {};
1056     var ctx = new openerp.base.CompoundContext(a_context).add(f_context);
1057     return ctx;
1058 }
1059
1060 openerp.base.form.FieldMany2One = openerp.base.form.Field.extend({
1061     init: function(view, node) {
1062         this._super(view, node);
1063         this.template = "FieldMany2One";
1064         this.limit = 7;
1065         this.value = null;
1066         this.cm_id = _.uniqueId('m2o_cm_');
1067         this.last_search = [];
1068     },
1069     start: function() {
1070         this._super();
1071         var self = this;
1072         this.$input = this.$element.find("input");
1073         this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
1074         this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
1075
1076         // context menu
1077         var bindings = {};
1078         bindings[this.cm_id + "_search"] = function() {
1079             self._search_create_popup("search");
1080         };
1081         bindings[this.cm_id + "_create"] = function() {
1082             self._search_create_popup("form");
1083         };
1084         bindings[this.cm_id + "_open"] = function() {
1085             if (!self.value) {
1086                 return;
1087             }
1088             self.session.action_manager.do_action({
1089                 "res_model": self.field.relation,
1090                 "views":[[false,"form"]],
1091                 "res_id": self.value[0],
1092                 "type":"ir.actions.act_window",
1093                 "view_type":"form",
1094                 "view_mode":"form",
1095                 "target":"new",
1096                 "context": build_relation_context(self)
1097             });
1098         };
1099         var cmenu = this.$menu_btn.contextMenu(this.cm_id, {'leftClickToo': true,
1100             bindings: bindings, itemStyle: {"color": ""},
1101             onContextMenu: function() {
1102                 if(self.value) {
1103                     $("#" + self.cm_id + "_open").removeClass("oe-m2o-disabled-cm");
1104                 } else {
1105                     $("#" + self.cm_id + "_open").addClass("oe-m2o-disabled-cm");
1106                 }
1107                 return true;
1108             }
1109         });
1110
1111         // some behavior for input
1112         this.$input.keyup(function() {
1113             if (self.$input.val() === "") {
1114                 self._change_int_value(null);
1115             } else if (self.value === null || (self.value && self.$input.val() !== self.value[1])) {
1116                 self._change_int_value(undefined);
1117             }
1118         });
1119         this.$drop_down.click(function() {
1120             if (self.$input.autocomplete("widget").is(":visible")) {
1121                 self.$input.autocomplete("close");
1122             } else {
1123                 if (self.value) {
1124                     self.$input.autocomplete("search", "");
1125                 } else {
1126                     self.$input.autocomplete("search");
1127                 }
1128                 self.$input.focus();
1129             }
1130         });
1131         var anyoneLoosesFocus = function() {
1132             if (!self.$input.is(":focus") &&
1133                     !self.$input.autocomplete("widget").is(":visible") &&
1134                     !self.value) {
1135                 if(self.value === undefined && self.last_search.length > 0) {
1136                     self._change_int_ext_value(self.last_search[0]);
1137                 } else {
1138                     self._change_int_ext_value(null);
1139                 }
1140             }
1141         }
1142         this.$input.focusout(anyoneLoosesFocus);
1143
1144         // autocomplete
1145         this.$input.autocomplete({
1146             source: function(req, resp) { self.get_search_result(req, resp); },
1147             select: function(event, ui) {
1148                 var item = ui.item;
1149                 if (item.id) {
1150                     self._change_int_value([item.id, item.name]);
1151                 } else if (item.action) {
1152                     self._change_int_value(undefined);
1153                     item.action();
1154                     return false;
1155                 }
1156             },
1157             focus: function(e, ui) {
1158                 e.preventDefault();
1159             },
1160             html: true,
1161             close: anyoneLoosesFocus,
1162             minLength: 0,
1163             delay: 0
1164         });
1165     },
1166     // autocomplete component content handling
1167     get_search_result: function(request, response) {
1168         var search_val = request.term;
1169         var self = this;
1170
1171         var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, []);
1172
1173         dataset.name_search([search_val, self.field.domain || [], 'ilike',
1174                 build_relation_context(self), this.limit + 1], function(data) {
1175             self.last_search = data.result;
1176             // possible selections for the m2o
1177             var values = _.map(data.result, function(x) {
1178                 return {label: $('<span />').text(x[1]).html(), name:x[1], id:x[0]};
1179             });
1180
1181             // search more... if more results that max
1182             if (values.length > self.limit) {
1183                 values = values.slice(0, self.limit);
1184                 values.push({label: "<em>   Search More...</em>", action: function() {
1185                     dataset.name_search([search_val, self.field.domain || [], 'ilike',
1186                             build_relation_context(self), false], function(data) {
1187                         self._change_int_value(null);
1188                         self._search_create_popup("search", data.result);
1189                     });
1190                 }});
1191             }
1192             // quick create
1193             var raw_result = _(data.result).map(function(x) {return x[1];})
1194             if (search_val.length > 0 &&
1195                 !_.include(raw_result, search_val) &&
1196                 (!self.value || search_val !== self.value[1])) {
1197                 values.push({label: '<em>   Create "<strong>' +
1198                         $('<span />').text(search_val).html() + '</strong>"</em>', action: function() {
1199                     self._quick_create(search_val);
1200                 }});
1201             }
1202             // create...
1203             values.push({label: "<em>   Create and Edit...</em>", action: function() {
1204                 self._change_int_value(null);
1205                 self._search_create_popup("form");
1206             }});
1207
1208             response(values);
1209         });
1210     },
1211     _quick_create: function(name) {
1212         var self = this;
1213         var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, []);
1214         dataset.call("name_create", [name, build_relation_context(self)], function(data) {
1215             self._change_int_ext_value(data.result);
1216         }, function(a, b) {
1217             self._change_int_value(null);
1218             self._search_create_popup("form", undefined, {"default_name": name});
1219         });
1220     },
1221     // all search/create popup handling
1222     _search_create_popup: function(view, ids, context) {
1223         var dataset = new openerp.base.DataSetStatic(this.session, this.field.relation, []);
1224         var self = this;
1225         var pop = new openerp.base.form.SelectCreatePopup(null, self.view.session);
1226         pop.select_element(self.field.relation,{
1227                 initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
1228                 initial_view: view,
1229                 disable_multiple_selection: true
1230                 }, self.view.domain || [],
1231                 new openerp.base.CompoundContext(build_relation_context(self)).add(context || {}));
1232         pop.on_select_elements.add(function(element_ids) {
1233             dataset.call("name_get", [element_ids[0]], function(data) {
1234                 self._change_int_ext_value(data.result[0]);
1235                 pop.stop();
1236             });
1237         });
1238     },
1239     _change_int_ext_value: function(value) {
1240         this._change_int_value(value);
1241         this.$input.val(this.value ? this.value[1] : "");
1242     },
1243     _change_int_value: function(value) {
1244         this.value = value;
1245         if (this.original_value === undefined || (this.value !== undefined &&
1246             ((this.original_value ? this.original_value[0] : null) !== (this.value ? this.value[0] : null)))) {
1247             this.original_value = undefined;
1248             this.on_ui_change();
1249         }
1250     },
1251     set_value_from_ui: function() {},
1252     set_value: function(value) {
1253         this._super(value);
1254         this.original_value = value;
1255         this._change_int_ext_value(value);
1256     },
1257     get_value: function() {
1258         if (this.value === undefined)
1259             throw "theorically unreachable state";
1260         return this.value ? this.value[0] : false;
1261     },
1262     validate: function() {
1263         this.invalid = false;
1264         if (this.value === null) {
1265             this.invalid = this.required;
1266         }
1267     }
1268 });
1269
1270 /*
1271 # Values: (0, 0,  { fields })    create
1272 #         (1, ID, { fields })    update
1273 #         (2, ID)                remove (delete)
1274 #         (3, ID)                unlink one (target id or target of relation)
1275 #         (4, ID)                link
1276 #         (5)                    unlink all (only valid for one2many)
1277 */
1278
1279 openerp.base.form.FieldOne2Many = openerp.base.form.Field.extend({
1280     init: function(view, node) {
1281         this._super(view, node);
1282         this.template = "FieldOne2Many";
1283         this.is_started = $.Deferred();
1284         this.is_setted = $.Deferred();
1285     },
1286     start: function() {
1287         this._super.apply(this, arguments);
1288
1289         var self = this;
1290
1291         this.dataset = new openerp.base.form.One2ManyDataset(this.session, this.field.relation);
1292         this.dataset.on_change.add_last(function() {
1293             self.on_ui_change();
1294             self.reload_current_view();
1295         });
1296
1297         var modes = this.node.attrs.mode;
1298         modes = !!modes ? modes.split(",") : ["tree", "form"];
1299         var views = [];
1300         _.each(modes, function(mode) {
1301             var view = {view_id: false, view_type: mode == "tree" ? "list" : mode};
1302             if (self.field.views && self.field.views[mode]) {
1303                 view.embedded_view = self.field.views[mode];
1304             }
1305             if(view.view_type === "list") {
1306                 view.options = {
1307                 };
1308             }
1309             views.push(view);
1310         });
1311         this.views = views;
1312
1313         this.viewmanager = new openerp.base.ViewManager(this.view.session,
1314             this.element_id, this.dataset, views);
1315         var reg = new openerp.base.Registry();
1316         reg.add("form", openerp.base.views.map["form"]);
1317         reg.add("graph", openerp.base.views.map["graph"]);
1318         reg.add("list", "openerp.base.form.One2ManyListView");
1319         this.viewmanager.registry = reg;
1320
1321         this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
1322             if (view_type == "list") {
1323                 controller.o2m = self;
1324             } else if (view_type == "form") {
1325                 // TODO niv
1326             }
1327             self.is_started.resolve();
1328         });
1329         this.viewmanager.start();
1330
1331         $.when(this.is_started, this.is_setted).then(function() {
1332             if (modes[0] == "tree") {
1333                 var view = self.viewmanager.views[self.viewmanager.active_view].controller;
1334                 view.reload_content();
1335             }
1336             // TODO niv: handle other types of views
1337         });
1338     },
1339     reload_current_view: function() {
1340         var self = this;
1341         var view = self.viewmanager.views[self.viewmanager.active_view].controller;
1342         if(self.viewmanager.active_view === "list") {
1343             view.reload_content();
1344         } else if (self.viewmanager.active_view === "form") {
1345             // TODO niv: but fme did not implemented delete in form view anyway
1346         }
1347     },
1348     set_value_from_ui: function() {},
1349     set_value: function(value) {
1350         if(value != false) {
1351             this.dataset.reset_ids(value);
1352             this.is_setted.resolve();
1353         }
1354     },
1355     get_value: function() {
1356         var val = _.map(this.dataset.to_delete, function(v, k) {return [2, parseInt(k, 10)];});
1357         var val = val.concat(_.map(this.dataset.to_create, function(x) {return [0, 0, x];}));
1358         return val;
1359     },
1360     validate: function() {
1361         this.invalid = false;
1362         // TODO niv
1363     }
1364 });
1365
1366 openerp.base.form.One2ManyDataset = openerp.base.DataSetStatic.extend({
1367     init: function() {
1368         this._super.apply(this, arguments);
1369         this.reset_ids([]);
1370     },
1371     create: function(data, callback, error_callback) {
1372     },
1373     write: function (id, data, callback) {
1374     },
1375     unlink: function(ids) {
1376         var self = this;
1377         this.set_ids(_.without.apply(_, [this.ids].concat(ids)));
1378         _.each(ids, function(x) {self.to_delete[x] = true;});
1379         this.on_change();
1380     },
1381     reset_ids: function(ids) {
1382         this.set_ids(ids);
1383         this.to_delete = {};
1384         this.to_create = {};
1385     },
1386     on_change: function() {}
1387 });
1388
1389 openerp.base.form.One2ManyListView = openerp.base.ListView.extend({
1390     do_add_record: function () {
1391         var self = this;
1392         var pop = new openerp.base.form.SelectCreatePopup(null, self.o2m.view.session);
1393         pop.select_element(self.o2m.field.relation,{
1394             initial_view: "form",
1395             alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined
1396         });
1397         pop.on_select_elements.add(function(element_ids) {
1398             var ids = self.o2m.dataset.ids;
1399             _.each(element_ids, function(x) {if (!_.include(ids, x)) ids.push(x);});
1400             self.o2m.dataset.set_ids(ids);
1401             self.o2m.on_ui_change();
1402             self.o2m.reload_current_view();
1403         });
1404     }
1405 });
1406
1407 openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({
1408     init: function(view, node) {
1409         this._super(view, node);
1410         this.template = "FieldMany2Many";
1411         this.list_id = _.uniqueId("many2many");
1412         this.is_started = $.Deferred();
1413         this.is_setted = $.Deferred();
1414     },
1415     start: function() {
1416         this._super.apply(this, arguments);
1417
1418         var self = this;
1419
1420         this.dataset = new openerp.base.DataSetStatic(
1421                 this.session, this.field.relation);
1422         this.dataset.on_unlink.add_last(function(ids) {
1423             self.list_view.reload_content();
1424
1425             self.on_ui_change();
1426         });
1427
1428         this.list_view = new openerp.base.form.Many2ManyListView(
1429                 null, this.view.session, this.list_id, this.dataset, false, {
1430                     'addable': 'Add'
1431             });
1432         this.list_view.m2m_field = this;
1433         this.list_view.on_loaded.add_last(function() {
1434             self.is_started.resolve();
1435         });
1436         this.list_view.start();
1437         $.when(this.is_started, this.is_setted).then(function() {
1438             self.list_view.reload_content();
1439         });
1440     },
1441     set_value: function(value) {
1442         if (value != false) {
1443             this.dataset.set_ids(value);
1444             this.is_setted.resolve();
1445         }
1446     },
1447     get_value: function() {
1448         return [[6,false,this.dataset.ids]];
1449     }
1450 });
1451
1452 openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({
1453     do_add_record: function () {
1454         var pop = new openerp.base.form.SelectCreatePopup(
1455                 null, this.m2m_field.view.session);
1456         pop.select_element(this.model);
1457         var self = this;
1458         pop.on_select_elements.add(function(element_ids) {
1459             _.each(element_ids, function(element_id) {
1460                 if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) {
1461                     self.dataset.set_ids([].concat(self.dataset.ids, [element_id]));
1462                     self.reload_content();
1463                 }
1464             });
1465             pop.stop();
1466         });
1467     },
1468     do_activate_record: function(index, id) {
1469         this.m2m_field.view.session.action_manager.do_action({
1470             "res_model": this.dataset.model,
1471             "views":[[false,"form"]],
1472             "res_id": id,
1473             "type":"ir.actions.act_window",
1474             "view_type":"form",
1475             "view_mode":"form",
1476             "target":"new"
1477         });
1478     }
1479 });
1480
1481 openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({
1482     identifier_prefix: "selectcreatepopup",
1483     template: "SelectCreatePopup",
1484     /**
1485      * options:
1486      * - initial_ids
1487      * - initial_view: form or search (default search)
1488      * - disable_multiple_selection
1489      * - alternative_form_view
1490      */
1491     select_element: function(model, options, domain, context) {
1492         this.model = model;
1493         this.domain = domain || [];
1494         this.context = context || {};
1495         this.options = options || {};
1496         this.initial_ids = this.options.initial_ids;
1497         jQuery(this.render()).dialog({title: '',
1498                     modal: true,
1499                     minWidth: 800});
1500         this.start();
1501     },
1502     start: function() {
1503         this._super();
1504         this.dataset = new openerp.base.DataSetSearch(this.session, this.model,
1505             this.context, this.domain);
1506         if ((this.options.initial_view || "search") == "search") {
1507             this.setup_search_view();
1508         } else { // "form"
1509             this.new_object();
1510         }
1511     },
1512     setup_search_view: function() {
1513         var self = this;
1514         if (this.searchview) {
1515             this.searchview.stop();
1516         }
1517         this.searchview = new openerp.base.SearchView(null, this.session,
1518                 this.element_id + "_search", this.dataset, false, {
1519                     "selectable": !this.options.disable_multiple_selection,
1520                     "deletable": false
1521                 });
1522         this.searchview.on_search.add(function(domains, contexts, groupbys) {
1523             if (self.initial_ids) {
1524                 self.view_list.do_search.call(self,[[["id", "in", self.initial_ids]]],
1525                     contexts, groupbys);
1526                 self.initial_ids = undefined;
1527             } else {
1528                 self.view_list.do_search.call(self, domains, contexts, groupbys);
1529             }
1530         });
1531         this.searchview.on_loaded.add_last(function () {
1532             var $buttons = self.searchview.$element.find(".oe_search-view-buttons");
1533             $buttons.append(QWeb.render("SelectCreatePopup.search.buttons"));
1534             var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close");
1535             $cbutton.click(function() {
1536                 self.stop();
1537             });
1538             var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select");
1539             if(self.options.disable_multiple_selection) {
1540                 $sbutton.hide();
1541             }
1542             $sbutton.click(function() {
1543                 self.on_select_elements(self.selected_ids);
1544             });
1545             self.view_list = new openerp.base.form.SelectCreateListView( null, self.session,
1546                     self.element_id + "_view_list", self.dataset, false,
1547                     {'deletable': false});
1548             self.view_list.popup = self;
1549             self.view_list.do_show();
1550             self.view_list.start().then(function() {
1551                 self.searchview.do_search();
1552             });
1553         });
1554         this.searchview.start();
1555     },
1556     on_select_elements: function(element_ids) {
1557     },
1558     on_click_element: function(ids) {
1559         this.selected_ids = ids || [];
1560         if(this.selected_ids.length > 0) {
1561             this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
1562         } else {
1563             this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
1564         }
1565     },
1566     new_object: function() {
1567         var self = this;
1568         if (this.searchview) {
1569             this.searchview.hide();
1570         }
1571         if (this.view_list) {
1572             this.view_list.$element.hide();
1573         }
1574         this.dataset.index = null;
1575         this.view_form = new openerp.base.FormView(null, this.session,
1576                 this.element_id + "_view_form", this.dataset, false);
1577         if (this.options.alternative_form_view) {
1578             this.view_form.set_embedded_view(this.options.alternative_form_view);
1579         }
1580         this.view_form.start();
1581         this.view_form.on_loaded.add_last(function() {
1582             var $buttons = self.view_form.$element.find(".oe_form_buttons");
1583             $buttons.html(QWeb.render("SelectCreatePopup.form.buttons"));
1584             var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save");
1585             $nbutton.click(function() {
1586                 self.view_form.do_save();
1587             });
1588             var $cbutton = $buttons.find(".oe_selectcreatepopup-form-close");
1589             $cbutton.click(function() {
1590                 self.stop();
1591             });
1592         });
1593         this.view_form.on_created.add_last(function(r, success) {
1594             if (r.result) {
1595                 var id = arguments[0].result;
1596                 self.on_select_elements([id]);
1597             }
1598         });
1599         this.view_form.do_show();
1600     }
1601 });
1602
1603 openerp.base.form.SelectCreateListView = openerp.base.ListView.extend({
1604     do_add_record: function () {
1605         this.popup.new_object();
1606     },
1607     select_record: function(index) {
1608         this.popup.on_select_elements([this.dataset.ids[index]]);
1609     },
1610     do_select: function(ids, records) {
1611         this._super(ids, records);
1612         this.popup.on_click_element(ids);
1613     }
1614 });
1615
1616 openerp.base.form.FieldReference = openerp.base.form.Field.extend({
1617     init: function(view, node) {
1618         this._super(view, node);
1619         this.template = "FieldReference";
1620     }
1621 });
1622
1623 openerp.base.form.FieldBinary = openerp.base.form.Field.extend({
1624     init: function(view, node) {
1625         this._super(view, node);
1626         this.iframe = this.element_id + '_iframe';
1627         this.binary_value = false;
1628     },
1629     start: function() {
1630         this._super.apply(this, arguments);
1631         this.$element.find('input.oe-binary-file').change(this.on_file_change);
1632         this.$element.find('button.oe-binary-file-save').click(this.on_save_as);
1633         this.$element.find('.oe-binary-file-clear').click(this.on_clear);
1634     },
1635     set_value_from_ui: function() {
1636     },
1637     human_filesize : function(size) {
1638         var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
1639         var i = 0;
1640         while (size >= 1024) {
1641             size /= 1024;
1642             ++i;
1643         }
1644         return size.toFixed(2) + ' ' + units[i];
1645     },
1646     on_file_change: function(e) {
1647         // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper
1648         // http://www.html5rocks.com/tutorials/file/dndfiles/
1649         // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler
1650         window[this.iframe] = this.on_file_uploaded;
1651         if ($(e.target).val() != '') {
1652             this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id);
1653             this.$element.find('form.oe-binary-form').submit();
1654             this.toggle_progress();
1655         }
1656     },
1657     toggle_progress: function() {
1658         this.$element.find('.oe-binary-progress, .oe-binary').toggle();
1659     },
1660     on_file_uploaded: function(size, name, content_type, file_base64) {
1661         delete(window[this.iframe]);
1662         if (size === false) {
1663             this.notification.warn("File Upload", "There was a problem while uploading your file");
1664             // TODO: use openerp web exception handler
1665             console.log("Error while uploading file : ", name);
1666         } else {
1667             this.on_file_uploaded_and_valid.apply(this, arguments);
1668             this.on_ui_change();
1669         }
1670         this.toggle_progress();
1671     },
1672     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
1673     },
1674     on_save_as: function() {
1675         if (!this.view.datarecord.id) {
1676             this.notification.warn("Can't save file", "The record has not yet been saved");
1677         } else {
1678             var url = '/base/binary/saveas?session_id=' + this.session.session_id + '&model=' +
1679                 this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name +
1680                 '&fieldname=' + (this.node.attrs.filename || '') + '&t=' + (new Date().getTime())
1681             window.open(url);
1682         }
1683     },
1684     on_clear: function() {
1685         if (this.value !== false) {
1686             this.value = false;
1687             this.binary_value = false;
1688             this.on_ui_change();
1689         }
1690         return false;
1691     }
1692 });
1693
1694 openerp.base.form.FieldBinaryFile = openerp.base.form.FieldBinary.extend({
1695     init: function(view, node) {
1696         this._super(view, node);
1697         this.template = "FieldBinaryFile";
1698     },
1699     set_value: function(value) {
1700         this._super.apply(this, arguments);
1701         var show_value = (value != null && value !== false) ? value : '';
1702         this.$element.find('input').eq(0).val(show_value);
1703     },
1704     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
1705         this.value = file_base64;
1706         this.binary_value = true;
1707         var show_value = this.human_filesize(size);
1708         this.$element.find('input').eq(0).val(show_value);
1709         this.set_filename(name);
1710     },
1711     set_filename: function(value) {
1712         var filename = this.node.attrs.filename;
1713         if (this.view.fields[filename]) {
1714             this.view.fields[filename].set_value(value);
1715             this.view.fields[filename].on_ui_change();
1716         }
1717     },
1718     on_clear: function() {
1719         this._super.apply(this, arguments);
1720         this.$element.find('input').eq(0).val('');
1721         this.set_filename('');
1722     }
1723 });
1724
1725 openerp.base.form.FieldBinaryImage = openerp.base.form.FieldBinary.extend({
1726     init: function(view, node) {
1727         this._super(view, node);
1728         this.template = "FieldBinaryImage";
1729     },
1730     start: function() {
1731         this._super.apply(this, arguments);
1732         this.$image = this.$element.find('img.oe-binary-image');
1733     },
1734     set_image_maxwidth: function() {
1735         this.$image.css('max-width', this.$element.width());
1736     },
1737     on_file_change: function() {
1738         this.set_image_maxwidth();
1739         this._super.apply(this, arguments);
1740     },
1741     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
1742         this.value = file_base64;
1743         this.binary_value = true;
1744         this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64);
1745     },
1746     on_clear: function() {
1747         this._super.apply(this, arguments);
1748         this.$image.attr('src', '/base/static/src/img/placeholder.png');
1749     },
1750     set_value: function(value) {
1751         this._super.apply(this, arguments);
1752         this.set_image_maxwidth();
1753         var url = '/base/binary/image?session_id=' + this.session.session_id + '&model=' +
1754             this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime())
1755         this.$image.attr('src', url);
1756     }
1757 });
1758
1759 /**
1760  * Registry of form widgets, called by :js:`openerp.base.FormView`
1761  */
1762 openerp.base.form.widgets = new openerp.base.Registry({
1763     'frame' : 'openerp.base.form.WidgetFrame',
1764     'group' : 'openerp.base.form.WidgetFrame',
1765     'notebook' : 'openerp.base.form.WidgetNotebook',
1766     'separator' : 'openerp.base.form.WidgetSeparator',
1767     'label' : 'openerp.base.form.WidgetLabel',
1768     'button' : 'openerp.base.form.WidgetButton',
1769     'char' : 'openerp.base.form.FieldChar',
1770     'email' : 'openerp.base.form.FieldEmail',
1771     'url' : 'openerp.base.form.FieldUrl',
1772     'text' : 'openerp.base.form.FieldText',
1773     'text_wiki' : 'openerp.base.form.FieldText',
1774     'date' : 'openerp.base.form.FieldDate',
1775     'datetime' : 'openerp.base.form.FieldDatetime',
1776     'selection' : 'openerp.base.form.FieldSelection',
1777     'many2one' : 'openerp.base.form.FieldMany2One',
1778     'many2many' : 'openerp.base.form.FieldMany2Many',
1779     'one2many' : 'openerp.base.form.FieldOne2Many',
1780     'one2many_list' : 'openerp.base.form.FieldOne2Many',
1781     'reference' : 'openerp.base.form.FieldReference',
1782     'boolean' : 'openerp.base.form.FieldBoolean',
1783     'float' : 'openerp.base.form.FieldFloat',
1784     'integer': 'openerp.base.form.FieldFloat',
1785     'progressbar': 'openerp.base.form.FieldProgressBar',
1786     'float_time': 'openerp.base.form.FieldFloatTime',
1787     'image': 'openerp.base.form.FieldBinaryImage',
1788     'binary': 'openerp.base.form.FieldBinaryFile'
1789 });
1790
1791 };
1792
1793 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: