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