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