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