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