[fix] problem with on_change and parent
[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 + 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                     this.log("on_processed_onchange can't find field " + f, result);
246                 }
247             }
248             this.on_form_changed();
249         }
250         if (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");
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     init: function(view, node) {
1448         this._super(view, node);
1449         this.template = "FieldOne2Many";
1450         this.is_started = $.Deferred();
1451     },
1452     start: function() {
1453         this._super.apply(this, arguments);
1454
1455         var self = this;
1456
1457         this.dataset = new openerp.base.form.One2ManyDataSet(this.session, this.field.relation);
1458         this.dataset.o2m = this;
1459         this.dataset.parent_view = this.view;
1460         this.dataset.on_change.add_last(function() {
1461             self.on_ui_change();
1462         });
1463
1464         var modes = this.node.attrs.mode;
1465         modes = !!modes ? modes.split(",") : ["tree", "form"];
1466         var views = [];
1467         _.each(modes, function(mode) {
1468             var view = {view_id: false, view_type: mode == "tree" ? "list" : mode};
1469             if (self.field.views && self.field.views[mode]) {
1470                 view.embedded_view = self.field.views[mode];
1471             }
1472             if(view.view_type === "list") {
1473                 view.options = {
1474                 };
1475             }
1476             views.push(view);
1477         });
1478         this.views = views;
1479
1480         this.viewmanager = new openerp.base.ViewManager(this.view.session,
1481             this.element_id, this.dataset, views);
1482         this.viewmanager.registry = openerp.base.views.clone({
1483             list: 'openerp.base.form.One2ManyListView'
1484         });
1485
1486         this.viewmanager.on_controller_inited.add_last(function(view_type, controller) {
1487             if (view_type == "list") {
1488                 controller.o2m = self;
1489             } else if (view_type == "form") {
1490                 // TODO niv
1491             }
1492             self.is_started.resolve();
1493         });
1494         this.viewmanager.start();
1495     },
1496     reload_current_view: function() {
1497         var self = this;
1498         var view = self.viewmanager.views[self.viewmanager.active_view].controller;
1499         if(self.viewmanager.active_view === "list") {
1500             view.reload_content();
1501         } else if (self.viewmanager.active_view === "form") {
1502             // TODO niv: implement
1503         }
1504     },
1505     set_value_from_ui: function() {},
1506     set_value: function(value) {
1507         value = value || [];
1508         var self = this;
1509         this.dataset.reset_ids([]);
1510         if(value.length >= 1 && value[0] instanceof Array) {
1511             var ids = [];
1512             _.each(value, function(command) {
1513                 var obj = {values: command[2]};
1514                 switch (command[0]) {
1515                     case commands.CREATE:
1516                         obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
1517                         self.dataset.to_create.push(obj);
1518                         self.dataset.cache.push(_.clone(obj));
1519                         ids.push(obj.id);
1520                         return;
1521                     case commands.UPDATE:
1522                         obj['id'] = command[1];
1523                         self.dataset.to_write.push(obj);
1524                         self.dataset.cache.push(_.clone(obj));
1525                         ids.push(obj.id);
1526                         return;
1527                     case commands.DELETE:
1528                         self.dataset.to_delete.push({id: command[1]});
1529                         return;
1530                     case commands.LINK_TO:
1531                         ids.push(command[1]);
1532                         return;
1533                     case commands.DELETE_ALL:
1534                         self.dataset.delete_all = true;
1535                         return;
1536                 }
1537             });
1538             this._super(ids);
1539             this.dataset.set_ids(ids);
1540         } else if (value.length >= 1 && typeof(value[0]) === "object") {
1541             var ids = [];
1542             this.dataset.delete_all = true;
1543             _.each(value, function(command) {
1544                 var obj = {values: command};
1545                 obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
1546                 self.dataset.to_create.push(obj);
1547                 self.dataset.cache.push(_.clone(obj));
1548                 ids.push(obj.id);
1549             });
1550             this._super(ids);
1551             this.dataset.set_ids(ids);
1552         } else {
1553             this._super(value);
1554             this.dataset.reset_ids(value);
1555         }
1556         $.when(this.is_started).then(function() {
1557             self.reload_current_view();
1558         });
1559     },
1560     get_value: function() {
1561         var self = this;
1562         if (!this.dataset)
1563             return [];
1564         var val = this.dataset.delete_all ? [commands.delete_all()] : [];
1565         val = val.concat(_.map(this.dataset.ids, function(id) {
1566             var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
1567             if (alter_order) {
1568                 return commands.create(alter_order.values);
1569             }
1570             alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
1571             if (alter_order) {
1572                 return commands.update(alter_order.id, alter_order.values);
1573             }
1574             return commands.link_to(id);
1575         }));
1576         return val.concat(_.map(
1577             this.dataset.to_delete, function(x) {
1578                 return commands['delete'](x.id);}));
1579     },
1580     validate: function() {
1581         this.invalid = false;
1582         // TODO niv
1583     }
1584 });
1585
1586 openerp.base.form.One2ManyDataSet = openerp.base.BufferedDataSet.extend({
1587     get_context: function() {
1588         this.context = this.o2m.build_context();
1589         return this.context;
1590     }
1591 });
1592
1593 openerp.base.form.One2ManyListView = openerp.base.ListView.extend({
1594     do_add_record: function () {
1595         if (this.options.editable) {
1596             this._super.apply(this, arguments);
1597         } else {
1598             var self = this;
1599             var pop = new openerp.base.form.SelectCreatePopup(null, self.o2m.view.session);
1600             pop.select_element(self.o2m.field.relation,{
1601                 initial_view: "form",
1602                 alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
1603                 auto_create: false,
1604                 parent_view: self.o2m.view
1605             }, self.o2m.build_domain(), self.o2m.build_context());
1606             pop.on_create.add(function(data) {
1607                 self.o2m.dataset.create(data, function(r) {
1608                     self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result]));
1609                     self.o2m.dataset.on_change();
1610                     pop.stop();
1611                     self.o2m.reload_current_view();
1612                 });
1613             });
1614         }
1615     }
1616 });
1617
1618 openerp.base.form.FieldMany2Many = openerp.base.form.Field.extend({
1619     init: function(view, node) {
1620         this._super(view, node);
1621         this.template = "FieldMany2Many";
1622         this.list_id = _.uniqueId("many2many");
1623         this.is_started = $.Deferred();
1624     },
1625     start: function() {
1626         this._super.apply(this, arguments);
1627
1628         var self = this;
1629
1630         this.dataset = new openerp.base.form.Many2ManyDataSet(
1631                 this.session, this.field.relation);
1632         this.dataset.m2m = this;
1633         this.dataset.on_unlink.add_last(function(ids) {
1634             self.on_ui_change();
1635         });
1636
1637         this.list_view = new openerp.base.form.Many2ManyListView(
1638                 null, this.view.session, this.list_id, this.dataset, false, {
1639                     'addable': 'Add'
1640             });
1641         this.list_view.m2m_field = this;
1642         this.list_view.on_loaded.add_last(function() {
1643             self.is_started.resolve();
1644         });
1645         this.list_view.start();
1646     },
1647     set_value: function(value) {
1648         value = value || [];
1649         if (value.length >= 1 && value[0] instanceof Array) {
1650             value = value[0][2];
1651         }
1652         this._super(value);
1653         this.dataset.set_ids(value);
1654         var self = this;
1655         $.when(this.is_started).then(function() {
1656             self.list_view.reload_content();
1657         });
1658     },
1659     get_value: function() {
1660         return [commands.replace_with(this.dataset.ids)];
1661     },
1662     set_value_from_ui: function() {},
1663     validate: function() {
1664         this.invalid = false;
1665         // TODO niv
1666     }
1667 });
1668
1669 openerp.base.form.Many2ManyDataSet = openerp.base.DataSetStatic.extend({
1670     get_context: function() {
1671         this.context = this.m2m.build_context();
1672         return this.context;
1673     }
1674 });
1675
1676 openerp.base.form.Many2ManyListView = openerp.base.ListView.extend({
1677     do_add_record: function () {
1678         var pop = new openerp.base.form.SelectCreatePopup(
1679                 null, this.m2m_field.view.session);
1680         pop.select_element(this.model, {}, this.m2m_field.build_domain(), this.m2m_field.build_context());
1681         var self = this;
1682         pop.on_select_elements.add(function(element_ids) {
1683             _.each(element_ids, function(element_id) {
1684                 if(! _.detect(self.dataset.ids, function(x) {return x == element_id;})) {
1685                     self.dataset.set_ids([].concat(self.dataset.ids, [element_id]));
1686                     self.m2m_field.on_ui_change();
1687                     self.reload_content();
1688                 }
1689             });
1690             pop.stop();
1691         });
1692     },
1693     do_activate_record: function(index, id) {
1694         this.m2m_field.view.session.action_manager.do_action({
1695             "res_model": this.dataset.model,
1696             "views": [[false,"form"]],
1697             "res_id": id,
1698             "type": "ir.actions.act_window",
1699             "view_type": "form",
1700             "view_mode": "form",
1701             "target": "new",
1702             "context": this.m2m_field.build_context()
1703         });
1704     }
1705 });
1706
1707 openerp.base.form.SelectCreatePopup = openerp.base.BaseWidget.extend({
1708     identifier_prefix: "selectcreatepopup",
1709     template: "SelectCreatePopup",
1710     /**
1711      * options:
1712      * - initial_ids
1713      * - initial_view: form or search (default search)
1714      * - disable_multiple_selection
1715      * - alternative_form_view
1716      * - auto_create (default true)
1717      * - parent_view
1718      */
1719     select_element: function(model, options, domain, context) {
1720         this.model = model;
1721         this.domain = domain || [];
1722         this.context = context || {};
1723         this.options = _.defaults(options || {}, {"initial_view": "search", "auto_create": true});
1724         this.initial_ids = this.options.initial_ids;
1725         jQuery(this.render()).dialog({title: '',
1726                     modal: true,
1727                     minWidth: 800});
1728         this.start();
1729     },
1730     start: function() {
1731         this._super();
1732         this.dataset = new openerp.base.ReadOnlyDataSetSearch(this.session, this.model,
1733             this.context, this.domain);
1734         this.dataset.parent_view = this.options.parent_view;
1735         if (this.options.initial_view == "search") {
1736             this.setup_search_view();
1737         } else { // "form"
1738             this.new_object();
1739         }
1740     },
1741     setup_search_view: function() {
1742         var self = this;
1743         if (this.searchview) {
1744             this.searchview.stop();
1745         }
1746         this.searchview = new openerp.base.SearchView(null, this.session,
1747                 this.element_id + "_search", this.dataset, false, {
1748                     "selectable": !this.options.disable_multiple_selection,
1749                     "deletable": false
1750                 });
1751         this.searchview.on_search.add(function(domains, contexts, groupbys) {
1752             if (self.initial_ids) {
1753                 self.view_list.do_search.call(self, domains.concat([[["id", "in", self.initial_ids]]]),
1754                     contexts, groupbys);
1755                 self.initial_ids = undefined;
1756             } else {
1757                 self.view_list.do_search.call(self, domains, contexts, groupbys);
1758             }
1759         });
1760         this.searchview.on_loaded.add_last(function () {
1761             var $buttons = self.searchview.$element.find(".oe_search-view-buttons");
1762             $buttons.append(QWeb.render("SelectCreatePopup.search.buttons"));
1763             var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close");
1764             $cbutton.click(function() {
1765                 self.stop();
1766             });
1767             var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select");
1768             if(self.options.disable_multiple_selection) {
1769                 $sbutton.hide();
1770             }
1771             $sbutton.click(function() {
1772                 self.on_select_elements(self.selected_ids);
1773             });
1774             self.view_list = new openerp.base.form.SelectCreateListView( null, self.session,
1775                     self.element_id + "_view_list", self.dataset, false,
1776                     {'deletable': false});
1777             self.view_list.popup = self;
1778             self.view_list.do_show();
1779             self.view_list.start().then(function() {
1780                 self.searchview.do_search();
1781             });
1782         });
1783         this.searchview.start();
1784     },
1785     on_create: function(data) {
1786         if (!this.options.auto_create)
1787             return;
1788         var self = this;
1789         var wdataset = new openerp.base.DataSetSearch(this.session, this.model, this.context, this.domain);
1790         wdataset = this.options.parent_view;
1791         wdataset.create(data, function(r) {
1792             self.on_select_elements([r.result]);
1793         });
1794     },
1795     on_select_elements: function(element_ids) {
1796     },
1797     on_click_element: function(ids) {
1798         this.selected_ids = ids || [];
1799         if(this.selected_ids.length > 0) {
1800             this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
1801         } else {
1802             this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
1803         }
1804     },
1805     new_object: function() {
1806         var self = this;
1807         if (this.searchview) {
1808             this.searchview.hide();
1809         }
1810         if (this.view_list) {
1811             this.view_list.$element.hide();
1812         }
1813         this.dataset.index = null;
1814         this.view_form = new openerp.base.FormView(null, this.session,
1815                 this.element_id + "_view_form", this.dataset, false);
1816         if (this.options.alternative_form_view) {
1817             this.view_form.set_embedded_view(this.options.alternative_form_view);
1818         }
1819         this.view_form.start();
1820         this.view_form.on_loaded.add_last(function() {
1821             var $buttons = self.view_form.$element.find(".oe_form_buttons");
1822             $buttons.html(QWeb.render("SelectCreatePopup.form.buttons"));
1823             var $nbutton = $buttons.find(".oe_selectcreatepopup-form-save");
1824             $nbutton.click(function() {
1825                 self.view_form.do_save();
1826             });
1827             var $cbutton = $buttons.find(".oe_selectcreatepopup-form-close");
1828             $cbutton.click(function() {
1829                 self.stop();
1830             });
1831         });
1832         this.dataset.on_create.add(this.on_create);
1833         this.view_form.do_show();
1834     }
1835 });
1836
1837 openerp.base.form.SelectCreateListView = openerp.base.ListView.extend({
1838     do_add_record: function () {
1839         this.popup.new_object();
1840     },
1841     select_record: function(index) {
1842         this.popup.on_select_elements([this.dataset.ids[index]]);
1843     },
1844     do_select: function(ids, records) {
1845         this._super(ids, records);
1846         this.popup.on_click_element(ids);
1847     }
1848 });
1849
1850 openerp.base.form.FieldReference = openerp.base.form.Field.extend({
1851     init: function(view, node) {
1852         this._super(view, node);
1853         this.template = "FieldReference";
1854     }
1855 });
1856
1857 openerp.base.form.FieldBinary = openerp.base.form.Field.extend({
1858     init: function(view, node) {
1859         this._super(view, node);
1860         this.iframe = this.element_id + '_iframe';
1861         this.binary_value = false;
1862     },
1863     start: function() {
1864         this._super.apply(this, arguments);
1865         this.$element.find('input.oe-binary-file').change(this.on_file_change);
1866         this.$element.find('button.oe-binary-file-save').click(this.on_save_as);
1867         this.$element.find('.oe-binary-file-clear').click(this.on_clear);
1868     },
1869     set_value_from_ui: function() {
1870     },
1871     human_filesize : function(size) {
1872         var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
1873         var i = 0;
1874         while (size >= 1024) {
1875             size /= 1024;
1876             ++i;
1877         }
1878         return size.toFixed(2) + ' ' + units[i];
1879     },
1880     on_file_change: function(e) {
1881         // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper
1882         // http://www.html5rocks.com/tutorials/file/dndfiles/
1883         // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler
1884         window[this.iframe] = this.on_file_uploaded;
1885         if ($(e.target).val() != '') {
1886             this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id);
1887             this.$element.find('form.oe-binary-form').submit();
1888             this.toggle_progress();
1889         }
1890     },
1891     toggle_progress: function() {
1892         this.$element.find('.oe-binary-progress, .oe-binary').toggle();
1893     },
1894     on_file_uploaded: function(size, name, content_type, file_base64) {
1895         delete(window[this.iframe]);
1896         if (size === false) {
1897             this.notification.warn("File Upload", "There was a problem while uploading your file");
1898             // TODO: use openerp web exception handler
1899             console.log("Error while uploading file : ", name);
1900         } else {
1901             this.on_file_uploaded_and_valid.apply(this, arguments);
1902             this.on_ui_change();
1903         }
1904         this.toggle_progress();
1905     },
1906     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
1907     },
1908     on_save_as: function() {
1909         if (!this.view.datarecord.id) {
1910             this.notification.warn("Can't save file", "The record has not yet been saved");
1911         } else {
1912             var url = '/base/binary/saveas?session_id=' + this.session.session_id + '&model=' +
1913                 this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name +
1914                 '&fieldname=' + (this.node.attrs.filename || '') + '&t=' + (new Date().getTime())
1915             window.open(url);
1916         }
1917     },
1918     on_clear: function() {
1919         if (this.value !== false) {
1920             this.value = false;
1921             this.binary_value = false;
1922             this.on_ui_change();
1923         }
1924         return false;
1925     }
1926 });
1927
1928 openerp.base.form.FieldBinaryFile = openerp.base.form.FieldBinary.extend({
1929     init: function(view, node) {
1930         this._super(view, node);
1931         this.template = "FieldBinaryFile";
1932     },
1933     set_value: function(value) {
1934         this._super.apply(this, arguments);
1935         var show_value = (value != null && value !== false) ? value : '';
1936         this.$element.find('input').eq(0).val(show_value);
1937     },
1938     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
1939         this.value = file_base64;
1940         this.binary_value = true;
1941         var show_value = this.human_filesize(size);
1942         this.$element.find('input').eq(0).val(show_value);
1943         this.set_filename(name);
1944     },
1945     set_filename: function(value) {
1946         var filename = this.node.attrs.filename;
1947         if (this.view.fields[filename]) {
1948             this.view.fields[filename].set_value(value);
1949             this.view.fields[filename].on_ui_change();
1950         }
1951     },
1952     on_clear: function() {
1953         this._super.apply(this, arguments);
1954         this.$element.find('input').eq(0).val('');
1955         this.set_filename('');
1956     }
1957 });
1958
1959 openerp.base.form.FieldBinaryImage = openerp.base.form.FieldBinary.extend({
1960     init: function(view, node) {
1961         this._super(view, node);
1962         this.template = "FieldBinaryImage";
1963     },
1964     start: function() {
1965         this._super.apply(this, arguments);
1966         this.$image = this.$element.find('img.oe-binary-image');
1967     },
1968     set_image_maxwidth: function() {
1969         this.$image.css('max-width', this.$element.width());
1970     },
1971     on_file_change: function() {
1972         this.set_image_maxwidth();
1973         this._super.apply(this, arguments);
1974     },
1975     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
1976         this.value = file_base64;
1977         this.binary_value = true;
1978         this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64);
1979     },
1980     on_clear: function() {
1981         this._super.apply(this, arguments);
1982         this.$image.attr('src', '/base/static/src/img/placeholder.png');
1983     },
1984     set_value: function(value) {
1985         this._super.apply(this, arguments);
1986         this.set_image_maxwidth();
1987         var url = '/base/binary/image?session_id=' + this.session.session_id + '&model=' +
1988             this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime())
1989         this.$image.attr('src', url);
1990     }
1991 });
1992
1993 /**
1994  * Registry of form widgets, called by :js:`openerp.base.FormView`
1995  */
1996 openerp.base.form.widgets = new openerp.base.Registry({
1997     'frame' : 'openerp.base.form.WidgetFrame',
1998     'group' : 'openerp.base.form.WidgetFrame',
1999     'notebook' : 'openerp.base.form.WidgetNotebook',
2000     'separator' : 'openerp.base.form.WidgetSeparator',
2001     'label' : 'openerp.base.form.WidgetLabel',
2002     'button' : 'openerp.base.form.WidgetButton',
2003     'char' : 'openerp.base.form.FieldChar',
2004     'email' : 'openerp.base.form.FieldEmail',
2005     'url' : 'openerp.base.form.FieldUrl',
2006     'text' : 'openerp.base.form.FieldText',
2007     'text_wiki' : 'openerp.base.form.FieldText',
2008     'date' : 'openerp.base.form.FieldDate',
2009     'datetime' : 'openerp.base.form.FieldDatetime',
2010     'selection' : 'openerp.base.form.FieldSelection',
2011     'many2one' : 'openerp.base.form.FieldMany2One',
2012     'many2many' : 'openerp.base.form.FieldMany2Many',
2013     'one2many' : 'openerp.base.form.FieldOne2Many',
2014     'one2many_list' : 'openerp.base.form.FieldOne2Many',
2015     'reference' : 'openerp.base.form.FieldReference',
2016     'boolean' : 'openerp.base.form.FieldBoolean',
2017     'float' : 'openerp.base.form.FieldFloat',
2018     'integer': 'openerp.base.form.FieldFloat',
2019     'progressbar': 'openerp.base.form.FieldProgressBar',
2020     'float_time': 'openerp.base.form.FieldFloatTime',
2021     'image': 'openerp.base.form.FieldBinaryImage',
2022     'binary': 'openerp.base.form.FieldBinaryFile'
2023 });
2024
2025 };
2026
2027 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: