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