~tempo-openerp/+junk/loewert-report-name

« back to all changes in this revision

Viewing changes to addons/web/static/src/js/view_form.js

  • Committer: jbe at tempo-consulting
  • Date: 2013-08-21 08:48:11 UTC
  • Revision ID: jbe@tempo-consulting.fr-20130821084811-913uo4l7b5ayxq8m
[NEW] Création de la branche trunk Loewert

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
openerp.web.form = function (instance) {
 
2
var _t = instance.web._t,
 
3
   _lt = instance.web._lt;
 
4
var QWeb = instance.web.qweb;
 
5
 
 
6
/** @namespace */
 
7
instance.web.form = {};
 
8
 
 
9
/**
 
10
 * Interface implemented by the form view or any other object
 
11
 * able to provide the features necessary for the fields to work.
 
12
 *
 
13
 * Properties:
 
14
 *     - display_invalid_fields : if true, all fields where is_valid() return true should
 
15
 *     be displayed as invalid.
 
16
 *     - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
 
17
 * Events:
 
18
 *     - view_content_has_changed : when the values of the fields have changed. When
 
19
 *     this event is triggered all fields should reprocess their modifiers.
 
20
 *     - field_changed:<field_name> : when the value of a field change, an event is triggered
 
21
 *     named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
 
22
 *     This event is not related to the on_change mechanism of OpenERP and is always called
 
23
 *     when the value of a field is setted or changed. This event is only triggered when the
 
24
 *     value of the field is syntactically valid, but it can be triggered when the value
 
25
 *     is sematically invalid (ie, when a required field is false). It is possible that an event
 
26
 *     about a precise field is never triggered even if that field exists in the view, in that
 
27
 *     case the value of the field is assumed to be false.
 
28
 */
 
29
instance.web.form.FieldManagerMixin = {
 
30
    /**
 
31
     * Must return the asked field as in fields_get.
 
32
     */
 
33
    get_field_desc: function(field_name) {},
 
34
    /**
 
35
     * Returns the current value of a field present in the view. See the get_value() method
 
36
     * method in FieldInterface for further information.
 
37
     */
 
38
    get_field_value: function(field_name) {},
 
39
    /**
 
40
    Gives new values for the fields contained in the view. The new values could not be setted
 
41
    right after the call to this method. Setting new values can trigger on_changes.
 
42
 
 
43
    @param (dict) values A dictonnary with key = field name and value = new value.
 
44
    @return (Deferred) Is resolved after all the values are setted.
 
45
    */
 
46
    set_values: function(values) {},
 
47
    /**
 
48
    Computes an OpenERP domain.
 
49
 
 
50
    @param (list) expression An OpenERP domain.
 
51
    @return (boolean) The computed value of the domain.
 
52
    */
 
53
    compute_domain: function(expression) {},
 
54
    /**
 
55
    Builds an evaluation context for the resolution of the fields' contexts. Please note
 
56
    the field are only supposed to use this context to evualuate their own, they should not
 
57
    extend it.
 
58
 
 
59
    @return (CompoundContext) An OpenERP context.
 
60
    */
 
61
    build_eval_context: function() {},
 
62
};
 
63
 
 
64
instance.web.views.add('form', 'instance.web.FormView');
 
65
/**
 
66
 * Properties:
 
67
 *      - actual_mode: always "view", "edit" or "create". Read-only property. Determines
 
68
 *      the mode used by the view.
 
69
 */
 
70
instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, {
 
71
    /**
 
72
     * Indicates that this view is not searchable, and thus that no search
 
73
     * view should be displayed (if there is one active).
 
74
     */
 
75
    searchable: false,
 
76
    template: "FormView",
 
77
    display_name: _lt('Form'),
 
78
    view_type: "form",
 
79
    /**
 
80
     * @constructs instance.web.FormView
 
81
     * @extends instance.web.View
 
82
     *
 
83
     * @param {instance.web.Session} session the current openerp session
 
84
     * @param {instance.web.DataSet} dataset the dataset this view will work with
 
85
     * @param {String} view_id the identifier of the OpenERP view object
 
86
     * @param {Object} options
 
87
     *                  - resize_textareas : [true|false|max_height]
 
88
     *
 
89
     * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance
 
90
     */
 
91
    init: function(parent, dataset, view_id, options) {
 
92
        var self = this;
 
93
        this._super(parent);
 
94
        this.ViewManager = parent;
 
95
        this.set_default_options(options);
 
96
        this.dataset = dataset;
 
97
        this.model = dataset.model;
 
98
        this.view_id = view_id || false;
 
99
        this.fields_view = {};
 
100
        this.fields = {};
 
101
        this.fields_order = [];
 
102
        this.datarecord = {};
 
103
        this.default_focus_field = null;
 
104
        this.default_focus_button = null;
 
105
        this.fields_registry = instance.web.form.widgets;
 
106
        this.tags_registry = instance.web.form.tags;
 
107
        this.widgets_registry = instance.web.form.custom_widgets;
 
108
        this.has_been_loaded = $.Deferred();
 
109
        this.translatable_fields = [];
 
110
        _.defaults(this.options, {
 
111
            "not_interactible_on_create": false,
 
112
            "initial_mode": "view",
 
113
            "disable_autofocus": false,
 
114
            "footer_to_buttons": false,
 
115
        });
 
116
        this.is_initialized = $.Deferred();
 
117
        this.mutating_mutex = new $.Mutex();
 
118
        this.on_change_list = [];
 
119
        this.save_list = [];
 
120
        this.reload_mutex = new $.Mutex();
 
121
        this.__clicked_inside = false;
 
122
        this.__blur_timeout = null;
 
123
        this.rendering_engine = new instance.web.form.FormRenderingEngine(this);
 
124
        self.set({actual_mode: self.options.initial_mode});
 
125
        this.has_been_loaded.done(function() {
 
126
            self.on("change:actual_mode", self, self.check_actual_mode);
 
127
            self.check_actual_mode();
 
128
            self.on("change:actual_mode", self, self.init_pager);
 
129
            self.init_pager();
 
130
        });
 
131
        self.on("load_record", self, self.load_record);
 
132
        instance.web.bus.on('clear_uncommitted_changes', this, function(e) {
 
133
            if (!this.can_be_discarded()) {
 
134
                e.preventDefault();
 
135
            }
 
136
        });
 
137
    },
 
138
    view_loading: function(r) {
 
139
        return this.load_form(r);
 
140
    },
 
141
    destroy: function() {
 
142
        _.each(this.get_widgets(), function(w) {
 
143
            w.off('focused blurred');
 
144
            w.destroy();
 
145
        });
 
146
        if (this.$el) {
 
147
            this.$el.off('.formBlur');
 
148
        }
 
149
        this._super();
 
150
    },
 
151
    load_form: function(data) {
 
152
        var self = this;
 
153
        if (!data) {
 
154
            throw new Error(_t("No data provided."));
 
155
        }
 
156
        if (this.arch) {
 
157
            throw "Form view does not support multiple calls to load_form";
 
158
        }
 
159
        this.fields_order = [];
 
160
        this.fields_view = data;
 
161
 
 
162
        this.rendering_engine.set_fields_registry(this.fields_registry);
 
163
        this.rendering_engine.set_tags_registry(this.tags_registry);
 
164
        this.rendering_engine.set_widgets_registry(this.widgets_registry);
 
165
        this.rendering_engine.set_fields_view(data);
 
166
        var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container');
 
167
        this.rendering_engine.render_to($dest);
 
168
 
 
169
        this.$el.on('mousedown.formBlur', function () {
 
170
            self.__clicked_inside = true;
 
171
        });
 
172
 
 
173
        this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self}));
 
174
        if (this.options.$buttons) {
 
175
            this.$buttons.appendTo(this.options.$buttons);
 
176
        } else {
 
177
            this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
 
178
        }
 
179
        this.$buttons.on('click', '.oe_form_button_create',
 
180
                         this.guard_active(this.on_button_create));
 
181
        this.$buttons.on('click', '.oe_form_button_edit',
 
182
                         this.guard_active(this.on_button_edit));
 
183
        this.$buttons.on('click', '.oe_form_button_save',
 
184
                         this.guard_active(this.on_button_save));
 
185
        this.$buttons.on('click', '.oe_form_button_cancel',
 
186
                         this.guard_active(this.on_button_cancel));
 
187
        if (this.options.footer_to_buttons) {
 
188
            this.$el.find('footer').appendTo(this.$buttons);
 
189
        }
 
190
 
 
191
        this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar');
 
192
        if (!this.sidebar && this.options.$sidebar) {
 
193
            this.sidebar = new instance.web.Sidebar(this);
 
194
            this.sidebar.appendTo(this.$sidebar);
 
195
            if (this.fields_view.toolbar) {
 
196
                this.sidebar.add_toolbar(this.fields_view.toolbar);
 
197
            }
 
198
            this.sidebar.add_items('other', _.compact([
 
199
                self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete },
 
200
                self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }
 
201
            ]));
 
202
        }
 
203
 
 
204
        this.has_been_loaded.resolve();
 
205
 
 
206
        // Add bounce effect on button 'Edit' when click on readonly page view.
 
207
        this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) {
 
208
            if(self.get("actual_mode") == "view") {
 
209
                var $button = self.options.$buttons.find(".oe_form_button_edit");
 
210
                $button.openerpBounce();
 
211
                e.stopPropagation();
 
212
                instance.web.bus.trigger('click', e);
 
213
            }
 
214
        });
 
215
        //bounce effect on red button when click on statusbar.
 
216
        this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) {
 
217
            if((self.get("actual_mode") == "view")) {
 
218
                var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'});
 
219
                $button.openerpBounce();
 
220
                e.stopPropagation();
 
221
            }
 
222
         });
 
223
        this.trigger('form_view_loaded', data);
 
224
        return $.when();
 
225
    },
 
226
    widgetFocused: function() {
 
227
        // Clear click flag if used to focus a widget
 
228
        this.__clicked_inside = false;
 
229
        if (this.__blur_timeout) {
 
230
            clearTimeout(this.__blur_timeout);
 
231
            this.__blur_timeout = null;
 
232
        }
 
233
    },
 
234
    widgetBlurred: function() {
 
235
        if (this.__clicked_inside) {
 
236
            // clicked in an other section of the form (than the currently
 
237
            // focused widget) => just ignore the blurring entirely?
 
238
            this.__clicked_inside = false;
 
239
            return;
 
240
        }
 
241
        var self = this;
 
242
        // clear timeout, if any
 
243
        this.widgetFocused();
 
244
        this.__blur_timeout = setTimeout(function () {
 
245
            self.trigger('blurred');
 
246
        }, 0);
 
247
    },
 
248
 
 
249
    do_load_state: function(state, warm) {
 
250
        if (state.id && this.datarecord.id != state.id) {
 
251
            if (this.dataset.get_id_index(state.id) === null) {
 
252
                this.dataset.ids.push(state.id);
 
253
            }
 
254
            this.dataset.select_id(state.id);
 
255
            this.do_show({ reload: warm });
 
256
        }
 
257
    },
 
258
    /**
 
259
     *
 
260
     * @param {Object} [options]
 
261
     * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view".
 
262
     * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record
 
263
     * @return {$.Deferred}
 
264
     */
 
265
    do_show: function (options) {
 
266
        var self = this;
 
267
        options = options || {};
 
268
        if (this.sidebar) {
 
269
            this.sidebar.$el.show();
 
270
        }
 
271
        if (this.$buttons) {
 
272
            this.$buttons.show();
 
273
        }
 
274
        this.$el.show().css({
 
275
            opacity: '0',
 
276
            filter: 'alpha(opacity = 0)'
 
277
        });
 
278
        this.$el.add(this.$buttons).removeClass('oe_form_dirty');
 
279
 
 
280
        var shown = this.has_been_loaded;
 
281
        if (options.reload !== false) {
 
282
            shown = shown.then(function() {
 
283
                if (self.dataset.index === null) {
 
284
                    // null index means we should start a new record
 
285
                    return self.on_button_new();
 
286
                }
 
287
                var fields = _.keys(self.fields_view.fields);
 
288
                fields.push('display_name');
 
289
                return self.dataset.read_index(fields, {
 
290
                    context: { 'bin_size': true, 'future_display_name' : true }
 
291
                }).then(function(r) {
 
292
                    self.trigger('load_record', r);
 
293
                });
 
294
            });
 
295
        }
 
296
        return shown.then(function() {
 
297
            self._actualize_mode(options.mode || self.options.initial_mode);
 
298
            self.$el.css({
 
299
                opacity: '1',
 
300
                filter: 'alpha(opacity = 100)'
 
301
            });
 
302
        });
 
303
    },
 
304
    do_hide: function () {
 
305
        if (this.sidebar) {
 
306
            this.sidebar.$el.hide();
 
307
        }
 
308
        if (this.$buttons) {
 
309
            this.$buttons.hide();
 
310
        }
 
311
        if (this.$pager) {
 
312
            this.$pager.hide();
 
313
        }
 
314
        this._super();
 
315
    },
 
316
    load_record: function(record) {
 
317
        var self = this, set_values = [];
 
318
        if (!record) {
 
319
            this.set({ 'title' : undefined });
 
320
            this.do_warn(_t("Form"), _t("The record could not be found in the database."), true);
 
321
            return $.Deferred().reject();
 
322
        }
 
323
        this.datarecord = record;
 
324
        this._actualize_mode();
 
325
        this.set({ 'title' : record.id ? record.display_name : _t("New") });
 
326
 
 
327
        _(this.fields).each(function (field, f) {
 
328
            field._dirty_flag = false;
 
329
            field._inhibit_on_change_flag = true;
 
330
            var result = field.set_value(self.datarecord[f] || false);
 
331
            field._inhibit_on_change_flag = false;
 
332
            set_values.push(result);
 
333
        });
 
334
        return $.when.apply(null, set_values).then(function() {
 
335
            if (!record.id) {
 
336
                // New record: Second pass in order to trigger the onchanges
 
337
                // respecting the fields order defined in the view
 
338
                _.each(self.fields_order, function(field_name) {
 
339
                    if (record[field_name] !== undefined) {
 
340
                        var field = self.fields[field_name];
 
341
                        field._dirty_flag = true;
 
342
                        self.do_onchange(field);
 
343
                    }
 
344
                });
 
345
            }
 
346
            self.on_form_changed();
 
347
            self.rendering_engine.init_fields();
 
348
            self.is_initialized.resolve();
 
349
            self.do_update_pager(record.id == null);
 
350
            if (self.sidebar) {
 
351
               self.sidebar.do_attachement_update(self.dataset, self.datarecord.id);
 
352
            }
 
353
            if (record.id) {
 
354
                self.do_push_state({id:record.id});
 
355
            } else {
 
356
                self.do_push_state({});
 
357
            }
 
358
            self.$el.add(self.$buttons).removeClass('oe_form_dirty');
 
359
            self.autofocus();
 
360
        });
 
361
    },
 
362
    /**
 
363
     * Loads and sets up the default values for the model as the current
 
364
     * record
 
365
     *
 
366
     * @return {$.Deferred}
 
367
     */
 
368
    load_defaults: function () {
 
369
        var self = this;
 
370
        var keys = _.keys(this.fields_view.fields);
 
371
        if (keys.length) {
 
372
            return this.dataset.default_get(keys).then(function(r) {
 
373
                self.trigger('load_record', r);
 
374
            });
 
375
        }
 
376
        return self.trigger('load_record', {});
 
377
    },
 
378
    on_form_changed: function() {
 
379
        this.trigger("view_content_has_changed");
 
380
    },
 
381
    do_notify_change: function() {
 
382
        this.$el.add(this.$buttons).addClass('oe_form_dirty');
 
383
    },
 
384
    execute_pager_action: function(action) {
 
385
        if (this.can_be_discarded()) {
 
386
            switch (action) {
 
387
                case 'first':
 
388
                    this.dataset.index = 0;
 
389
                    break;
 
390
                case 'previous':
 
391
                    this.dataset.previous();
 
392
                    break;
 
393
                case 'next':
 
394
                    this.dataset.next();
 
395
                    break;
 
396
                case 'last':
 
397
                    this.dataset.index = this.dataset.ids.length - 1;
 
398
                    break;
 
399
            }
 
400
            this.reload();
 
401
            this.trigger('pager_action_executed');
 
402
        }
 
403
    },
 
404
    init_pager: function() {
 
405
        var self = this;
 
406
        if (this.$pager)
 
407
            this.$pager.remove();
 
408
        if (this.get("actual_mode") === "create")
 
409
            return;
 
410
        this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide();
 
411
        if (this.options.$pager) {
 
412
            this.$pager.appendTo(this.options.$pager);
 
413
        } else {
 
414
            this.$el.find('.oe_form_pager').replaceWith(this.$pager);
 
415
        }
 
416
        this.$pager.on('click','a[data-pager-action]',function() {
 
417
            var action = $(this).data('pager-action');
 
418
            self.execute_pager_action(action);
 
419
        });
 
420
        this.do_update_pager();
 
421
    },
 
422
    do_update_pager: function(hide_index) {
 
423
        this.$pager.toggle(this.dataset.ids.length > 1);
 
424
        if (hide_index) {
 
425
            $(".oe_form_pager_state", this.$pager).html("");
 
426
        } else {
 
427
            $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length));
 
428
        }
 
429
    },
 
430
    parse_on_change: function (on_change, widget) {
 
431
        var self = this;
 
432
        var onchange = _.str.trim(on_change);
 
433
        var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/);
 
434
        if (!call) {
 
435
            throw new Error(_.str.sprintf( _t("Wrong on change format: %s"), onchange ));
 
436
        }
 
437
 
 
438
        var method = call[1];
 
439
        if (!_.str.trim(call[2])) {
 
440
            return {method: method, args: []}
 
441
        }
 
442
 
 
443
        var argument_replacement = {
 
444
            'False': function () {return false;},
 
445
            'True': function () {return true;},
 
446
            'None': function () {return null;},
 
447
            'context': function () {
 
448
                return new instance.web.CompoundContext(
 
449
                        self.dataset.get_context(),
 
450
                        widget.build_context() ? widget.build_context() : {});
 
451
            }
 
452
        };
 
453
        var parent_fields = null;
 
454
        var args = _.map(call[2].split(','), function (a, i) {
 
455
            var field = _.str.trim(a);
 
456
 
 
457
            // literal constant or context
 
458
            if (field in argument_replacement) {
 
459
                return argument_replacement[field]();
 
460
            }
 
461
            // literal number
 
462
            if (/^-?\d+(\.\d+)?$/.test(field)) {
 
463
                return Number(field);
 
464
            }
 
465
            // form field
 
466
            if (self.fields[field]) {
 
467
                var value_ = self.fields[field].get_value();
 
468
                return value_ == null ? false : value_;
 
469
            }
 
470
            // parent field
 
471
            var splitted = field.split('.');
 
472
            if (splitted.length > 1 && _.str.trim(splitted[0]) === "parent" && self.dataset.parent_view) {
 
473
                if (parent_fields === null) {
 
474
                    parent_fields = self.dataset.parent_view.get_fields_values();
 
475
                }
 
476
                var p_val = parent_fields[_.str.trim(splitted[1])];
 
477
                if (p_val !== undefined) {
 
478
                    return p_val == null ? false : p_val;
 
479
                }
 
480
            }
 
481
            // string literal
 
482
            var first_char = field[0], last_char = field[field.length-1];
 
483
            if ((first_char === '"' && last_char === '"')
 
484
                || (first_char === "'" && last_char === "'")) {
 
485
                return field.slice(1, -1);
 
486
            }
 
487
 
 
488
            throw new Error("Could not get field with name '" + field +
 
489
                            "' for onchange '" + onchange + "'");
 
490
        });
 
491
 
 
492
        return {
 
493
            method: method,
 
494
            args: args
 
495
        };
 
496
    },
 
497
    do_onchange: function(widget, processed) {
 
498
        var self = this;
 
499
        this.on_change_list = [{widget: widget, processed: processed}].concat(this.on_change_list);
 
500
        return this._process_operations();
 
501
    },
 
502
    _process_onchange: function(on_change_obj) {
 
503
        var self = this;
 
504
        var widget = on_change_obj.widget;
 
505
        var processed = on_change_obj.processed;
 
506
        try {
 
507
            var def;
 
508
            processed = processed || [];
 
509
            processed.push(widget.name);
 
510
            var on_change = widget.node.attrs.on_change;
 
511
            if (on_change) {
 
512
                var change_spec = self.parse_on_change(on_change, widget);
 
513
                var ids = [];
 
514
                if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) {
 
515
                    // In case of a o2m virtual id, we should pass an empty ids list
 
516
                    ids.push(self.datarecord.id);
 
517
                }
 
518
                def = self.alive(new instance.web.Model(self.dataset.model).call(
 
519
                    change_spec.method, [ids].concat(change_spec.args)));
 
520
            } else {
 
521
                def = $.when({});
 
522
            }
 
523
            return def.then(function(response) {
 
524
                if (widget.field['change_default']) {
 
525
                    var fieldname = widget.name;
 
526
                    var value_;
 
527
                    if (response.value && (fieldname in response.value)) {
 
528
                        // Use value from onchange if onchange executed
 
529
                        value_ = response.value[fieldname];
 
530
                    } else {
 
531
                        // otherwise get form value for field
 
532
                        value_ = self.fields[fieldname].get_value();
 
533
                    }
 
534
                    var condition = fieldname + '=' + value_;
 
535
 
 
536
                    if (value_) {
 
537
                        return self.alive(new instance.web.Model('ir.values').call(
 
538
                            'get_defaults', [self.model, condition]
 
539
                        )).then(function (results) {
 
540
                            if (!results.length) {
 
541
                                return response;
 
542
                            }
 
543
                            if (!response.value) {
 
544
                                response.value = {};
 
545
                            }
 
546
                            for(var i=0; i<results.length; ++i) {
 
547
                                // [whatever, key, value]
 
548
                                var triplet = results[i];
 
549
                                response.value[triplet[1]] = triplet[2];
 
550
                            }
 
551
                            return response;
 
552
                        });
 
553
                    }
 
554
                }
 
555
                return response;
 
556
            }).then(function(response) {
 
557
                return self.on_processed_onchange(response, processed);
 
558
            });
 
559
        } catch(e) {
 
560
            console.error(e);
 
561
            instance.webclient.crashmanager.show_message(e);
 
562
            return $.Deferred().reject();
 
563
        }
 
564
    },
 
565
    on_processed_onchange: function(result, processed) {
 
566
        try {
 
567
        if (result.value) {
 
568
            this._internal_set_values(result.value, processed);
 
569
        }
 
570
        if (!_.isEmpty(result.warning)) {
 
571
            instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), {
 
572
                title:result.warning.title,
 
573
                modal: true,
 
574
                buttons: [
 
575
                    {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
 
576
                ]
 
577
            });
 
578
        }
 
579
 
 
580
        var fields = this.fields;
 
581
        _(result.domain).each(function (domain, fieldname) {
 
582
            var field = fields[fieldname];
 
583
            if (!field) { return; }
 
584
            field.node.attrs.domain = domain;
 
585
        });
 
586
 
 
587
        return $.Deferred().resolve();
 
588
        } catch(e) {
 
589
            console.error(e);
 
590
            instance.webclient.crashmanager.show_message(e);
 
591
            return $.Deferred().reject();
 
592
        }
 
593
    },
 
594
    _process_operations: function() {
 
595
        var self = this;
 
596
        return this.mutating_mutex.exec(function() {
 
597
            function iterate() {
 
598
                var on_change_obj = self.on_change_list.shift();
 
599
                if (on_change_obj) {
 
600
                    return self._process_onchange(on_change_obj).then(function() {
 
601
                        return iterate();
 
602
                    });
 
603
                }
 
604
                var defs = [];
 
605
                _.each(self.fields, function(field) {
 
606
                    defs.push(field.commit_value());
 
607
                });
 
608
                var args = _.toArray(arguments);
 
609
                return $.when.apply($, defs).then(function() {
 
610
                    if (self.on_change_list.length !== 0) {
 
611
                        return iterate();
 
612
                    }
 
613
                    var save_obj = self.save_list.pop();
 
614
                    if (save_obj) {
 
615
                        return self._process_save(save_obj).then(function() {
 
616
                            save_obj.ret = _.toArray(arguments);
 
617
                            return iterate();
 
618
                        }, function() {
 
619
                            save_obj.error = true;
 
620
                        });
 
621
                    }
 
622
                    return $.when();
 
623
                });
 
624
            };
 
625
            return iterate();
 
626
        });
 
627
    },
 
628
    _internal_set_values: function(values, exclude) {
 
629
        exclude = exclude || [];
 
630
        for (var f in values) {
 
631
            if (!values.hasOwnProperty(f)) { continue; }
 
632
            var field = this.fields[f];
 
633
            // If field is not defined in the view, just ignore it
 
634
            if (field) {
 
635
                var value_ = values[f];
 
636
                if (field.get_value() != value_) {
 
637
                    field._inhibit_on_change_flag = true;
 
638
                    field.set_value(value_);
 
639
                    field._inhibit_on_change_flag = false;
 
640
                    field._dirty_flag = true;
 
641
                    if (!_.contains(exclude, field.name)) {
 
642
                        this.do_onchange(field, exclude);
 
643
                    }
 
644
                }
 
645
            }
 
646
        }
 
647
        this.on_form_changed();
 
648
    },
 
649
    set_values: function(values) {
 
650
        var self = this;
 
651
        return this.mutating_mutex.exec(function() {
 
652
            self._internal_set_values(values);
 
653
        });
 
654
    },
 
655
    /**
 
656
     * Ask the view to switch to view mode if possible. The view may not do it
 
657
     * if the current record is not yet saved. It will then stay in create mode.
 
658
     */
 
659
    to_view_mode: function() {
 
660
        this._actualize_mode("view");
 
661
    },
 
662
    /**
 
663
     * Ask the view to switch to edit mode if possible. The view may not do it
 
664
     * if the current record is not yet saved. It will then stay in create mode.
 
665
     */
 
666
    to_edit_mode: function() {
 
667
        this._actualize_mode("edit");
 
668
    },
 
669
    /**
 
670
     * Ask the view to switch to a precise mode if possible. The view is free to
 
671
     * not respect this command if the state of the dataset is not compatible with
 
672
     * the new mode. For example, it is not possible to switch to edit mode if
 
673
     * the current record is not yet saved in database.
 
674
     *
 
675
     * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If
 
676
     * undefined the view will test the actual mode to check if it is still consistent
 
677
     * with the dataset state.
 
678
     */
 
679
    _actualize_mode: function(switch_to) {
 
680
        var mode = switch_to || this.get("actual_mode");
 
681
        if (! this.datarecord.id) {
 
682
            mode = "create";
 
683
        } else if (mode === "create") {
 
684
            mode = "edit";
 
685
        }
 
686
        this.set({actual_mode: mode});
 
687
    },
 
688
    check_actual_mode: function(source, options) {
 
689
        var self = this;
 
690
        if(this.get("actual_mode") === "view") {
 
691
            self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly');
 
692
            self.$buttons.find('.oe_form_buttons_edit').hide();
 
693
            self.$buttons.find('.oe_form_buttons_view').show();
 
694
            self.$sidebar.show();
 
695
        } else {
 
696
            self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable');
 
697
            self.$buttons.find('.oe_form_buttons_edit').show();
 
698
            self.$buttons.find('.oe_form_buttons_view').hide();
 
699
            self.$sidebar.hide();
 
700
            this.autofocus();
 
701
        }
 
702
    },
 
703
    autofocus: function() {
 
704
        if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) {
 
705
            var fields_order = this.fields_order.slice(0);
 
706
            if (this.default_focus_field) {
 
707
                fields_order.unshift(this.default_focus_field.name);
 
708
            }
 
709
            for (var i = 0; i < fields_order.length; i += 1) {
 
710
                var field = this.fields[fields_order[i]];
 
711
                if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) {
 
712
                    if (field.focus() !== false) {
 
713
                        break;
 
714
                    }
 
715
                }
 
716
            }
 
717
        }
 
718
    },
 
719
    on_button_save: function() {
 
720
        var self = this;
 
721
        return this.save().done(function(result) {
 
722
            self.trigger("save", result);
 
723
            self.to_view_mode();
 
724
        }).then(function(result) {
 
725
            self.ViewManager.ActionManager.__parentedParent.menu.do_reload_needaction();
 
726
        });
 
727
    },
 
728
    on_button_cancel: function(event) {
 
729
        if (this.can_be_discarded()) {
 
730
            if (this.get('actual_mode') === 'create') {
 
731
                this.trigger('history_back');
 
732
            } else {
 
733
                this.to_view_mode();
 
734
                this.trigger('load_record', this.datarecord);
 
735
            }
 
736
        }
 
737
        this.trigger('on_button_cancel');
 
738
        return false;
 
739
    },
 
740
    on_button_new: function() {
 
741
        var self = this;
 
742
        this.to_edit_mode();
 
743
        return $.when(this.has_been_loaded).then(function() {
 
744
            if (self.can_be_discarded()) {
 
745
                return self.load_defaults();
 
746
            }
 
747
        });
 
748
    },
 
749
    on_button_edit: function() {
 
750
        return this.to_edit_mode();
 
751
    },
 
752
    on_button_create: function() {
 
753
        this.dataset.index = null;
 
754
        this.do_show();
 
755
    },
 
756
    on_button_duplicate: function() {
 
757
        var self = this;
 
758
        return this.has_been_loaded.then(function() {
 
759
            return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) {
 
760
                self.record_created(new_id);
 
761
                self.to_edit_mode();
 
762
            });
 
763
        });
 
764
    },
 
765
    on_button_delete: function() {
 
766
        var self = this;
 
767
        var def = $.Deferred();
 
768
        this.has_been_loaded.done(function() {
 
769
            if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) {
 
770
                self.dataset.unlink([self.datarecord.id]).done(function() {
 
771
                    if (self.dataset.size()) {
 
772
                        self.execute_pager_action('next');
 
773
                    } else {
 
774
                        self.do_action('history_back');
 
775
                    }
 
776
                    def.resolve();
 
777
                });
 
778
            } else {
 
779
                $.async_when().done(function () {
 
780
                    def.reject();
 
781
                })
 
782
            }
 
783
        });
 
784
        return def.promise();
 
785
    },
 
786
    can_be_discarded: function() {
 
787
        if (this.$el.is('.oe_form_dirty')) {
 
788
            if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) {
 
789
                return false;
 
790
            }
 
791
            this.$el.removeClass('oe_form_dirty');
 
792
        }
 
793
        return true;
 
794
    },
 
795
    /**
 
796
     * Triggers saving the form's record. Chooses between creating a new
 
797
     * record or saving an existing one depending on whether the record
 
798
     * already has an id property.
 
799
     *
 
800
     * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new
 
801
     * record, should that record be inserted at the start of the dataset (by
 
802
     * default, records are added at the end)
 
803
     */
 
804
    save: function(prepend_on_create) {
 
805
        var self = this;
 
806
        var save_obj = {prepend_on_create: prepend_on_create, ret: null};
 
807
        this.save_list.push(save_obj);
 
808
        return this._process_operations().then(function() {
 
809
            if (save_obj.error)
 
810
                return $.Deferred().reject();
 
811
            return $.when.apply($, save_obj.ret);
 
812
        }).done(function() {
 
813
            self.$el.removeClass('oe_form_dirty');
 
814
        });
 
815
    },
 
816
    _process_save: function(save_obj) {
 
817
        var self = this;
 
818
        var prepend_on_create = save_obj.prepend_on_create;
 
819
        try {
 
820
            var form_invalid = false,
 
821
                values = {},
 
822
                first_invalid_field = null,
 
823
                readonly_values = {};
 
824
            for (var f in self.fields) {
 
825
                if (!self.fields.hasOwnProperty(f)) { continue; }
 
826
                f = self.fields[f];
 
827
                if (!f.is_valid()) {
 
828
                    form_invalid = true;
 
829
                    if (!first_invalid_field) {
 
830
                        first_invalid_field = f;
 
831
                    }
 
832
                } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) {
 
833
                    // Special case 'id' field, do not save this field
 
834
                    // on 'create' : save all non readonly fields
 
835
                    // on 'edit' : save non readonly modified fields
 
836
                    if (!f.get("readonly")) {
 
837
                        values[f.name] = f.get_value();
 
838
                    } else {
 
839
                        readonly_values[f.name] = f.get_value();
 
840
                    }
 
841
                }
 
842
            }
 
843
            if (form_invalid) {
 
844
                self.set({'display_invalid_fields': true});
 
845
                first_invalid_field.focus();
 
846
                self.on_invalid();
 
847
                return $.Deferred().reject();
 
848
            } else {
 
849
                self.set({'display_invalid_fields': false});
 
850
                var save_deferral;
 
851
                if (!self.datarecord.id) {
 
852
                    // Creation save
 
853
                    save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) {
 
854
                        return self.record_created(r, prepend_on_create);
 
855
                    }, null);
 
856
                } else if (_.isEmpty(values)) {
 
857
                    // Not dirty, noop save
 
858
                    save_deferral = $.Deferred().resolve({}).promise();
 
859
                } else {
 
860
                    // Write save
 
861
                    save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) {
 
862
                        return self.record_saved(r);
 
863
                    }, null);
 
864
                }
 
865
                return save_deferral;
 
866
            }
 
867
        } catch (e) {
 
868
            console.error(e);
 
869
            return $.Deferred().reject();
 
870
        }
 
871
    },
 
872
    on_invalid: function() {
 
873
        var warnings = _(this.fields).chain()
 
874
            .filter(function (f) { return !f.is_valid(); })
 
875
            .map(function (f) {
 
876
                return _.str.sprintf('<li>%s</li>',
 
877
                    _.escape(f.string));
 
878
            }).value();
 
879
        warnings.unshift('<ul>');
 
880
        warnings.push('</ul>');
 
881
        this.do_warn(_t("The following fields are invalid:"), warnings.join(''));
 
882
    },
 
883
    /**
 
884
     * Reload the form after saving
 
885
     *
 
886
     * @param {Object} r result of the write function.
 
887
     */
 
888
    record_saved: function(r) {
 
889
        var self = this;
 
890
        if (!r) {
 
891
            // should not happen in the server, but may happen for internal purpose
 
892
            this.trigger('record_saved', r);
 
893
            return $.Deferred().reject();
 
894
        } else {
 
895
            return $.when(this.reload()).then(function () {
 
896
                self.trigger('record_saved', r);
 
897
                return r;
 
898
            });
 
899
        }
 
900
    },
 
901
    /**
 
902
     * Updates the form' dataset to contain the new record:
 
903
     *
 
904
     * * Adds the newly created record to the current dataset (at the end by
 
905
     *   default)
 
906
     * * Selects that record (sets the dataset's index to point to the new
 
907
     *   record's id).
 
908
     * * Updates the pager and sidebar displays
 
909
     *
 
910
     * @param {Object} r
 
911
     * @param {Boolean} [prepend_on_create=false] adds the newly created record
 
912
     * at the beginning of the dataset instead of the end
 
913
     */
 
914
    record_created: function(r, prepend_on_create) {
 
915
        var self = this;
 
916
        if (!r) {
 
917
            // should not happen in the server, but may happen for internal purpose
 
918
            this.trigger('record_created', r);
 
919
            return $.Deferred().reject();
 
920
        } else {
 
921
            this.datarecord.id = r;
 
922
            if (!prepend_on_create) {
 
923
                this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id]));
 
924
                this.dataset.index = this.dataset.ids.length - 1;
 
925
            } else {
 
926
                this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids));
 
927
                this.dataset.index = 0;
 
928
            }
 
929
            this.do_update_pager();
 
930
            if (this.sidebar) {
 
931
                this.sidebar.do_attachement_update(this.dataset, this.datarecord.id);
 
932
            }
 
933
            //openerp.log("The record has been created with id #" + this.datarecord.id);
 
934
            return $.when(this.reload()).then(function () {
 
935
                self.trigger('record_created', r);
 
936
                return _.extend(r, {created: true});
 
937
            });
 
938
        }
 
939
    },
 
940
    on_action: function (action) {
 
941
        console.debug('Executing action', action);
 
942
    },
 
943
    reload: function() {
 
944
        var self = this;
 
945
        return this.reload_mutex.exec(function() {
 
946
            if (self.dataset.index == null) {
 
947
                self.trigger("previous_view");
 
948
                return $.Deferred().reject().promise();
 
949
            }
 
950
            if (self.dataset.index == null || self.dataset.index < 0) {
 
951
                return $.when(self.on_button_new());
 
952
            } else {
 
953
                var fields = _.keys(self.fields_view.fields);
 
954
                fields.push('display_name');
 
955
                return self.dataset.read_index(fields,
 
956
                    {
 
957
                        context: {
 
958
                            'bin_size': true,
 
959
                            'future_display_name': true
 
960
                        }
 
961
                    }).then(function(r) {
 
962
                        self.trigger('load_record', r);
 
963
                    });
 
964
            }
 
965
        });
 
966
    },
 
967
    get_widgets: function() {
 
968
        return _.filter(this.getChildren(), function(obj) {
 
969
            return obj instanceof instance.web.form.FormWidget;
 
970
        });
 
971
    },
 
972
    get_fields_values: function() {
 
973
        var values = {};
 
974
        var ids = this.get_selected_ids();
 
975
        values["id"] = ids.length > 0 ? ids[0] : false;
 
976
        _.each(this.fields, function(value_, key) {
 
977
            values[key] = value_.get_value();
 
978
        });
 
979
        return values;
 
980
    },
 
981
    get_selected_ids: function() {
 
982
        var id = this.dataset.ids[this.dataset.index];
 
983
        return id ? [id] : [];
 
984
    },
 
985
    recursive_save: function() {
 
986
        var self = this;
 
987
        return $.when(this.save()).then(function(res) {
 
988
            if (self.dataset.parent_view)
 
989
                return self.dataset.parent_view.recursive_save();
 
990
        });
 
991
    },
 
992
    recursive_reload: function() {
 
993
        var self = this;
 
994
        var pre = $.when();
 
995
        if (self.dataset.parent_view)
 
996
                pre = self.dataset.parent_view.recursive_reload();
 
997
        return pre.then(function() {
 
998
            return self.reload();
 
999
        });
 
1000
    },
 
1001
    is_dirty: function() {
 
1002
        return _.any(this.fields, function (value_) {
 
1003
            return value_._dirty_flag;
 
1004
        });
 
1005
    },
 
1006
    is_interactible_record: function() {
 
1007
        var id = this.datarecord.id;
 
1008
        if (!id) {
 
1009
            if (this.options.not_interactible_on_create)
 
1010
                return false;
 
1011
        } else if (typeof(id) === "string") {
 
1012
            if(instance.web.BufferedDataSet.virtual_id_regex.test(id))
 
1013
                return false;
 
1014
        }
 
1015
        return true;
 
1016
    },
 
1017
    sidebar_eval_context: function () {
 
1018
        return $.when(this.build_eval_context());
 
1019
    },
 
1020
    open_defaults_dialog: function () {
 
1021
        var self = this;
 
1022
        var display = function (field, value) {
 
1023
            if (field instanceof instance.web.form.FieldSelection) {
 
1024
                return _(field.values).find(function (option) {
 
1025
                    return option[0] === value;
 
1026
                })[1];
 
1027
            } else if (field instanceof instance.web.form.FieldMany2One) {
 
1028
                return field.get_displayed();
 
1029
            }
 
1030
            return value;
 
1031
        }
 
1032
        var fields = _.chain(this.fields)
 
1033
            .map(function (field) {
 
1034
                var value = field.get_value();
 
1035
                // ignore fields which are empty, invisible, readonly, o2m
 
1036
                // or m2m
 
1037
                if (!value
 
1038
                        || field.get('invisible')
 
1039
                        || field.get("readonly")
 
1040
                        || field.field.type === 'one2many'
 
1041
                        || field.field.type === 'many2many'
 
1042
                        || field.field.type === 'binary'
 
1043
                        || field.password) {
 
1044
                    return false;
 
1045
                }
 
1046
 
 
1047
                return {
 
1048
                    name: field.name,
 
1049
                    string: field.string,
 
1050
                    value: value,
 
1051
                    displayed: display(field, value),
 
1052
                }
 
1053
            })
 
1054
            .compact()
 
1055
            .sortBy(function (field) { return field.string; })
 
1056
            .value();
 
1057
        var conditions = _.chain(self.fields)
 
1058
            .filter(function (field) { return field.field.change_default; })
 
1059
            .map(function (field) {
 
1060
                var value = field.get_value();
 
1061
                return {
 
1062
                    name: field.name,
 
1063
                    string: field.string,
 
1064
                    value: value,
 
1065
                    displayed: display(field, value),
 
1066
                }
 
1067
            })
 
1068
            .value();
 
1069
 
 
1070
        var d = new instance.web.Dialog(this, {
 
1071
            title: _t("Set Default"),
 
1072
            args: {
 
1073
                fields: fields,
 
1074
                conditions: conditions
 
1075
            },
 
1076
            buttons: [
 
1077
                {text: _t("Close"), click: function () { d.close(); }},
 
1078
                {text: _t("Save default"), click: function () {
 
1079
                    var $defaults = d.$el.find('#formview_default_fields');
 
1080
                    var field_to_set = $defaults.val();
 
1081
                    if (!field_to_set) {
 
1082
                        $defaults.parent().addClass('oe_form_invalid');
 
1083
                        return;
 
1084
                    }
 
1085
                    var condition = d.$el.find('#formview_default_conditions').val(),
 
1086
                        all_users = d.$el.find('#formview_default_all').is(':checked');
 
1087
                    new instance.web.DataSet(self, 'ir.values').call(
 
1088
                        'set_default', [
 
1089
                            self.dataset.model,
 
1090
                            field_to_set,
 
1091
                            self.fields[field_to_set].get_value(),
 
1092
                            all_users,
 
1093
                            true,
 
1094
                            condition || false
 
1095
                    ]).done(function () { d.close(); });
 
1096
                }}
 
1097
            ]
 
1098
        });
 
1099
        d.template = 'FormView.set_default';
 
1100
        d.open();
 
1101
    },
 
1102
    register_field: function(field, name) {
 
1103
        this.fields[name] = field;
 
1104
        this.fields_order.push(name);
 
1105
        if (JSON.parse(field.node.attrs.default_focus || "0")) {
 
1106
            this.default_focus_field = field;
 
1107
        }
 
1108
 
 
1109
        field.on('focused', null, this.proxy('widgetFocused'))
 
1110
             .on('blurred', null, this.proxy('widgetBlurred'));
 
1111
        if (this.get_field_desc(name).translate) {
 
1112
            this.translatable_fields.push(field);
 
1113
        }
 
1114
        field.on('changed_value', this, function() {
 
1115
            if (field.is_syntax_valid()) {
 
1116
                this.trigger('field_changed:' + name);
 
1117
            }
 
1118
            if (field._inhibit_on_change_flag) {
 
1119
                return;
 
1120
            }
 
1121
            field._dirty_flag = true;
 
1122
            if (field.is_syntax_valid()) {
 
1123
                this.do_onchange(field);
 
1124
                this.on_form_changed(true);
 
1125
                this.do_notify_change();
 
1126
            }
 
1127
        });
 
1128
    },
 
1129
    get_field_desc: function(field_name) {
 
1130
        return this.fields_view.fields[field_name];
 
1131
    },
 
1132
    get_field_value: function(field_name) {
 
1133
        return this.fields[field_name].get_value();
 
1134
    },
 
1135
    compute_domain: function(expression) {
 
1136
        return instance.web.form.compute_domain(expression, this.fields);
 
1137
    },
 
1138
    _build_view_fields_values: function() {
 
1139
        var a_dataset = this.dataset;
 
1140
        var fields_values = this.get_fields_values();
 
1141
        var active_id = a_dataset.ids[a_dataset.index];
 
1142
        _.extend(fields_values, {
 
1143
            active_id: active_id || false,
 
1144
            active_ids: active_id ? [active_id] : [],
 
1145
            active_model: a_dataset.model,
 
1146
            parent: {}
 
1147
        });
 
1148
        if (a_dataset.parent_view) {
 
1149
            fields_values.parent = a_dataset.parent_view.get_fields_values();
 
1150
        }
 
1151
        return fields_values;
 
1152
    },
 
1153
    build_eval_context: function() {
 
1154
        var a_dataset = this.dataset;
 
1155
        return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values());
 
1156
    },
 
1157
});
 
1158
 
 
1159
/**
 
1160
 * Interface to be implemented by rendering engines for the form view.
 
1161
 */
 
1162
instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({
 
1163
    set_fields_view: function(fields_view) {},
 
1164
    set_fields_registry: function(fields_registry) {},
 
1165
    render_to: function($el) {},
 
1166
});
 
1167
 
 
1168
/**
 
1169
 * Default rendering engine for the form view.
 
1170
 *
 
1171
 * It is necessary to set the view using set_view() before usage.
 
1172
 */
 
1173
instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({
 
1174
    init: function(view) {
 
1175
        this.view = view;
 
1176
    },
 
1177
    set_fields_view: function(fvg) {
 
1178
        this.fvg = fvg;
 
1179
        this.version = parseFloat(this.fvg.arch.attrs.version);
 
1180
        if (isNaN(this.version)) {
 
1181
            this.version = 6.1;
 
1182
        }
 
1183
    },
 
1184
    set_tags_registry: function(tags_registry) {
 
1185
        this.tags_registry = tags_registry;
 
1186
    },
 
1187
    set_fields_registry: function(fields_registry) {
 
1188
        this.fields_registry = fields_registry;
 
1189
    },
 
1190
    set_widgets_registry: function(widgets_registry) {
 
1191
        this.widgets_registry = widgets_registry;
 
1192
    },
 
1193
    // Backward compatibility tools, current default version: v6.1
 
1194
    process_version: function() {
 
1195
        if (this.version < 7.0) {
 
1196
            this.$form.find('form:first').wrapInner('<group col="4"/>');
 
1197
            this.$form.find('page').each(function() {
 
1198
                if (!$(this).parents('field').length) {
 
1199
                    $(this).wrapInner('<group col="4"/>');
 
1200
                }
 
1201
            });
 
1202
        }
 
1203
    },
 
1204
    get_arch_fragment: function() {
 
1205
        var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
 
1206
        // IE won't allow custom button@type and will revert it to spec default : 'submit'
 
1207
        $('button', doc).each(function() {
 
1208
            $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
 
1209
        });
 
1210
        // IE's html parser is also a css parser. How convenient...
 
1211
        $('board', doc).each(function() {
 
1212
            $(this).attr('layout', $(this).attr('style'));
 
1213
        });
 
1214
        return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
 
1215
    },
 
1216
    render_to: function($target) {
 
1217
        var self = this;
 
1218
        this.$target = $target;
 
1219
 
 
1220
        this.$form = this.get_arch_fragment();
 
1221
 
 
1222
        this.process_version();
 
1223
 
 
1224
        this.fields_to_init = [];
 
1225
        this.tags_to_init = [];
 
1226
        this.widgets_to_init = [];
 
1227
        this.labels = {};
 
1228
        this.process(this.$form);
 
1229
 
 
1230
        this.$form.appendTo(this.$target);
 
1231
 
 
1232
        this.to_replace = [];
 
1233
 
 
1234
        _.each(this.fields_to_init, function($elem) {
 
1235
            var name = $elem.attr("name");
 
1236
            if (!self.fvg.fields[name]) {
 
1237
                throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name));
 
1238
            }
 
1239
            var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]);
 
1240
            if (!obj) {
 
1241
                throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget')));
 
1242
            }
 
1243
            var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
 
1244
            var $label = self.labels[$elem.attr("name")];
 
1245
            if ($label) {
 
1246
                w.set_input_id($label.attr("for"));
 
1247
            }
 
1248
            self.alter_field(w);
 
1249
            self.view.register_field(w, $elem.attr("name"));
 
1250
            self.to_replace.push([w, $elem]);
 
1251
        });
 
1252
        _.each(this.tags_to_init, function($elem) {
 
1253
            var tag_name = $elem[0].tagName.toLowerCase();
 
1254
            var obj = self.tags_registry.get_object(tag_name);
 
1255
            var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
 
1256
            self.to_replace.push([w, $elem]);
 
1257
        });
 
1258
        _.each(this.widgets_to_init, function($elem) {
 
1259
            var widget_type = $elem.attr("type");
 
1260
            var obj = self.widgets_registry.get_object(widget_type);
 
1261
            var w = new (obj)(self.view, instance.web.xml_to_json($elem[0]));
 
1262
            self.to_replace.push([w, $elem]);
 
1263
        });
 
1264
    },
 
1265
    init_fields: function() {
 
1266
        var defs = [];
 
1267
        _.each(this.to_replace, function(el) {
 
1268
            defs.push(el[0].replace(el[1]));
 
1269
        });
 
1270
        this.to_replace = [];
 
1271
        return $.when.apply($, defs);
 
1272
    },
 
1273
    render_element: function(template /* dictionaries */) {
 
1274
        var dicts = [].slice.call(arguments).slice(1);
 
1275
        var dict = _.extend.apply(_, dicts);
 
1276
        dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb
 
1277
        return $(QWeb.render(template, dict));
 
1278
    },
 
1279
    alter_field: function(field) {
 
1280
    },
 
1281
    toggle_layout_debugging: function() {
 
1282
        if (!this.$target.has('.oe_layout_debug_cell:first').length) {
 
1283
            this.$target.find('[title]').removeAttr('title');
 
1284
            this.$target.find('.oe_form_group_cell').each(function() {
 
1285
                var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan');
 
1286
                $(this).attr('title', text);
 
1287
            });
 
1288
        }
 
1289
        this.$target.toggleClass('oe_layout_debugging');
 
1290
    },
 
1291
    process: function($tag) {
 
1292
        var self = this;
 
1293
        var tagname = $tag[0].nodeName.toLowerCase();
 
1294
        if (this.tags_registry.contains(tagname)) {
 
1295
            this.tags_to_init.push($tag);
 
1296
            return $tag;
 
1297
        }
 
1298
        var fn = self['process_' + tagname];
 
1299
        if (fn) {
 
1300
            var args = [].slice.call(arguments);
 
1301
            args[0] = $tag;
 
1302
            return fn.apply(self, args);
 
1303
        } else {
 
1304
            // generic tag handling, just process children
 
1305
            $tag.children().each(function() {
 
1306
                self.process($(this));
 
1307
            });
 
1308
            self.handle_common_properties($tag, $tag);
 
1309
            $tag.removeAttr("modifiers");
 
1310
            return $tag;
 
1311
        }
 
1312
    },
 
1313
    process_widget: function($widget) {
 
1314
        this.widgets_to_init.push($widget);
 
1315
        return $widget;
 
1316
    },
 
1317
    process_sheet: function($sheet) {
 
1318
        var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes());
 
1319
        this.handle_common_properties($new_sheet, $sheet);
 
1320
        var $dst = $new_sheet.find('.oe_form_sheet');
 
1321
        $sheet.contents().appendTo($dst);
 
1322
        $sheet.before($new_sheet).remove();
 
1323
        this.process($new_sheet);
 
1324
    },
 
1325
    process_form: function($form) {
 
1326
        if ($form.find('> sheet').length === 0) {
 
1327
            $form.addClass('oe_form_nosheet');
 
1328
        }
 
1329
        var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
 
1330
        this.handle_common_properties($new_form, $form);
 
1331
        $form.contents().appendTo($new_form);
 
1332
        if ($form[0] === this.$form[0]) {
 
1333
            // If root element, replace it
 
1334
            this.$form = $new_form;
 
1335
        } else {
 
1336
            $form.before($new_form).remove();
 
1337
        }
 
1338
        this.process($new_form);
 
1339
    },
 
1340
    /*
 
1341
     * Used by direct <field> children of a <group> tag only
 
1342
     * This method will add the implicit <label...> for every field
 
1343
     * in the <group>
 
1344
    */
 
1345
    preprocess_field: function($field) {
 
1346
        var self = this;
 
1347
        var name = $field.attr('name'),
 
1348
            field_colspan = parseInt($field.attr('colspan'), 10),
 
1349
            field_modifiers = JSON.parse($field.attr('modifiers') || '{}');
 
1350
 
 
1351
        if ($field.attr('nolabel') === '1')
 
1352
            return;
 
1353
        $field.attr('nolabel', '1');
 
1354
        var found = false;
 
1355
        this.$form.find('label[for="' + name + '"]').each(function(i ,el) {
 
1356
            $(el).parents().each(function(unused, tag) {
 
1357
                var name = tag.tagName.toLowerCase();
 
1358
                if (name === "field" || name in self.tags_registry.map)
 
1359
                    found = true;
 
1360
            });
 
1361
        });
 
1362
        if (found)
 
1363
            return;
 
1364
 
 
1365
        var $label = $('<label/>').attr({
 
1366
            'for' : name,
 
1367
            "modifiers": JSON.stringify({invisible: field_modifiers.invisible}),
 
1368
            "string": $field.attr('string'),
 
1369
            "help": $field.attr('help'),
 
1370
            "class": $field.attr('class'),
 
1371
        });
 
1372
        $label.insertBefore($field);
 
1373
        if (field_colspan > 1) {
 
1374
            $field.attr('colspan', field_colspan - 1);
 
1375
        }
 
1376
        return $label;
 
1377
    },
 
1378
    process_field: function($field) {
 
1379
        if ($field.parent().is('group')) {
 
1380
            // No implicit labels for normal fields, only for <group> direct children
 
1381
            var $label = this.preprocess_field($field);
 
1382
            if ($label) {
 
1383
                this.process($label);
 
1384
            }
 
1385
        }
 
1386
        this.fields_to_init.push($field);
 
1387
        return $field;
 
1388
    },
 
1389
    process_group: function($group) {
 
1390
        var self = this;
 
1391
        $group.children('field').each(function() {
 
1392
            self.preprocess_field($(this));
 
1393
        });
 
1394
        var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes());
 
1395
        var $table;
 
1396
        if ($new_group.first().is('table.oe_form_group')) {
 
1397
            $table = $new_group;
 
1398
        } else if ($new_group.filter('table.oe_form_group').length) {
 
1399
            $table = $new_group.filter('table.oe_form_group').first();
 
1400
        } else {
 
1401
            $table = $new_group.find('table.oe_form_group').first();
 
1402
        }
 
1403
 
 
1404
        var $tr, $td,
 
1405
            cols = parseInt($group.attr('col') || 2, 10),
 
1406
            row_cols = cols;
 
1407
 
 
1408
        var children = [];
 
1409
        $group.children().each(function(a,b,c) {
 
1410
            var $child = $(this);
 
1411
            var colspan = parseInt($child.attr('colspan') || 1, 10);
 
1412
            var tagName = $child[0].tagName.toLowerCase();
 
1413
            var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
 
1414
            var newline = tagName === 'newline';
 
1415
 
 
1416
            // Note FME: those classes are used in layout debug mode
 
1417
            if ($tr && row_cols > 0 && (newline || row_cols < colspan)) {
 
1418
                $tr.addClass('oe_form_group_row_incomplete');
 
1419
                if (newline) {
 
1420
                    $tr.addClass('oe_form_group_row_newline');
 
1421
                }
 
1422
            }
 
1423
            if (newline) {
 
1424
                $tr = null;
 
1425
                return;
 
1426
            }
 
1427
            if (!$tr || row_cols < colspan) {
 
1428
                $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table);
 
1429
                row_cols = cols;
 
1430
            } else if (tagName==='group') {
 
1431
                // When <group> <group/><group/> </group>, we need a spacing between the two groups
 
1432
                $td.addClass('oe_group_right')
 
1433
            }
 
1434
            row_cols -= colspan;
 
1435
 
 
1436
            // invisibility transfer
 
1437
            var field_modifiers = JSON.parse($child.attr('modifiers') || '{}');
 
1438
            var invisible = field_modifiers.invisible;
 
1439
            self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible})));
 
1440
 
 
1441
            $tr.append($td.append($child));
 
1442
            children.push($child[0]);
 
1443
        });
 
1444
        if (row_cols && $td) {
 
1445
            $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
 
1446
        }
 
1447
        $group.before($new_group).remove();
 
1448
 
 
1449
        $table.find('> tbody > tr').each(function() {
 
1450
            var to_compute = [],
 
1451
                row_cols = cols,
 
1452
                total = 100;
 
1453
            $(this).children().each(function() {
 
1454
                var $td = $(this),
 
1455
                    $child = $td.children(':first');
 
1456
                if ($child.attr('cell-class')) {
 
1457
                    $td.addClass($child.attr('cell-class'));
 
1458
                }
 
1459
                switch ($child[0].tagName.toLowerCase()) {
 
1460
                    case 'separator':
 
1461
                        break;
 
1462
                    case 'label':
 
1463
                        if ($child.attr('for')) {
 
1464
                            $td.attr('width', '1%').addClass('oe_form_group_cell_label');
 
1465
                            row_cols-= $td.attr('colspan') || 1;
 
1466
                            total--;
 
1467
                        }
 
1468
                        break;
 
1469
                    default:
 
1470
                        var width = _.str.trim($child.attr('width') || ''),
 
1471
                            iwidth = parseInt(width, 10);
 
1472
                        if (iwidth) {
 
1473
                            if (width.substr(-1) === '%') {
 
1474
                                total -= iwidth;
 
1475
                                width = iwidth + '%';
 
1476
                            } else {
 
1477
                                // Absolute width
 
1478
                                $td.css('min-width', width + 'px');
 
1479
                            }
 
1480
                            $td.attr('width', width);
 
1481
                            $child.removeAttr('width');
 
1482
                            row_cols-= $td.attr('colspan') || 1;
 
1483
                        } else {
 
1484
                            to_compute.push($td);
 
1485
                        }
 
1486
 
 
1487
                }
 
1488
            });
 
1489
            if (row_cols) {
 
1490
                var unit = Math.floor(total / row_cols);
 
1491
                if (!$(this).is('.oe_form_group_row_incomplete')) {
 
1492
                    _.each(to_compute, function($td, i) {
 
1493
                        var width = parseInt($td.attr('colspan'), 10) * unit;
 
1494
                        $td.attr('width', width + '%');
 
1495
                        total -= width;
 
1496
                    });
 
1497
                }
 
1498
            }
 
1499
        });
 
1500
        _.each(children, function(el) {
 
1501
            self.process($(el));
 
1502
        });
 
1503
        this.handle_common_properties($new_group, $group);
 
1504
        return $new_group;
 
1505
    },
 
1506
    process_notebook: function($notebook) {
 
1507
        var self = this;
 
1508
        var pages = [];
 
1509
        $notebook.find('> page').each(function() {
 
1510
            var $page = $(this);
 
1511
            var page_attrs = $page.getAttributes();
 
1512
            page_attrs.id = _.uniqueId('notebook_page_');
 
1513
            var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs);
 
1514
            $page.contents().appendTo($new_page);
 
1515
            $page.before($new_page).remove();
 
1516
            var ic = self.handle_common_properties($new_page, $page).invisibility_changer;
 
1517
            page_attrs.__page = $new_page;
 
1518
            page_attrs.__ic = ic;
 
1519
            pages.push(page_attrs);
 
1520
 
 
1521
            $new_page.children().each(function() {
 
1522
                self.process($(this));
 
1523
            });
 
1524
        });
 
1525
        var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages });
 
1526
        $notebook.contents().appendTo($new_notebook);
 
1527
        $notebook.before($new_notebook).remove();
 
1528
        self.process($($new_notebook.children()[0]));
 
1529
        //tabs and invisibility handling
 
1530
        $new_notebook.tabs();
 
1531
        _.each(pages, function(page, i) {
 
1532
            if (! page.__ic)
 
1533
                return;
 
1534
            page.__ic.on("change:effective_invisible", null, function() {
 
1535
                if (!page.__ic.get('effective_invisible') && page.autofocus) {
 
1536
                    $new_notebook.tabs('select', i);
 
1537
                    return;
 
1538
                }
 
1539
                var current = $new_notebook.tabs("option", "selected");
 
1540
                if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible"))
 
1541
                    return;
 
1542
                var first_visible = _.find(_.range(pages.length), function(i2) {
 
1543
                    return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible"));
 
1544
                });
 
1545
                if (first_visible !== undefined) {
 
1546
                    $new_notebook.tabs('select', first_visible);
 
1547
                }
 
1548
            });
 
1549
        });
 
1550
 
 
1551
        this.handle_common_properties($new_notebook, $notebook);
 
1552
        return $new_notebook;
 
1553
    },
 
1554
    process_separator: function($separator) {
 
1555
        var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes());
 
1556
        $separator.before($new_separator).remove();
 
1557
        this.handle_common_properties($new_separator, $separator);
 
1558
        return $new_separator;
 
1559
    },
 
1560
    process_label: function($label) {
 
1561
        var name = $label.attr("for"),
 
1562
            field_orm = this.fvg.fields[name];
 
1563
        var dict = {
 
1564
            string: $label.attr('string') || (field_orm || {}).string || '',
 
1565
            help: $label.attr('help') || (field_orm || {}).help || '',
 
1566
            _for: name ? _.uniqueId('oe-field-input-') : undefined,
 
1567
        };
 
1568
        var align = parseFloat(dict.align);
 
1569
        if (isNaN(align) || align === 1) {
 
1570
            align = 'right';
 
1571
        } else if (align === 0) {
 
1572
            align = 'left';
 
1573
        } else {
 
1574
            align = 'center';
 
1575
        }
 
1576
        dict.align = align;
 
1577
        var $new_label = this.render_element('FormRenderingLabel', dict);
 
1578
        $label.before($new_label).remove();
 
1579
        this.handle_common_properties($new_label, $label);
 
1580
        if (name) {
 
1581
            this.labels[name] = $new_label;
 
1582
        }
 
1583
        return $new_label;
 
1584
    },
 
1585
    handle_common_properties: function($new_element, $node) {
 
1586
        var str_modifiers = $node.attr("modifiers") || "{}";
 
1587
        var modifiers = JSON.parse(str_modifiers);
 
1588
        var ic = null;
 
1589
        if (modifiers.invisible !== undefined)
 
1590
            ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element);
 
1591
        $new_element.addClass($node.attr("class") || "");
 
1592
        $new_element.attr('style', $node.attr('style'));
 
1593
        return {invisibility_changer: ic,};
 
1594
    },
 
1595
});
 
1596
 
 
1597
/**
 
1598
    Welcome.
 
1599
 
 
1600
    If you read this documentation, it probably means that you were asked to use a form view widget outside of
 
1601
    a form view. Before going further, you must understand that those fields were never really created for
 
1602
    that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
 
1603
    you to hack the system with more style.
 
1604
*/
 
1605
instance.web.form.DefaultFieldManager = instance.web.Widget.extend({
 
1606
    init: function(parent, eval_context) {
 
1607
        this._super(parent);
 
1608
        this.field_descs = {};
 
1609
        this.eval_context = eval_context || {};
 
1610
        this.set({
 
1611
            display_invalid_fields: false,
 
1612
            actual_mode: 'create',
 
1613
        });
 
1614
    },
 
1615
    get_field_desc: function(field_name) {
 
1616
        if (this.field_descs[field_name] === undefined) {
 
1617
            this.field_descs[field_name] = {
 
1618
                string: field_name,
 
1619
            };
 
1620
        }
 
1621
        return this.field_descs[field_name];
 
1622
    },
 
1623
    extend_field_desc: function(fields) {
 
1624
        var self = this;
 
1625
        _.each(fields, function(v, k) {
 
1626
            _.extend(self.get_field_desc(k), v);
 
1627
        });
 
1628
    },
 
1629
    get_field_value: function(field_name) {
 
1630
        return false;
 
1631
    },
 
1632
    set_values: function(values) {
 
1633
        // nothing
 
1634
    },
 
1635
    compute_domain: function(expression) {
 
1636
        return instance.web.form.compute_domain(expression, {});
 
1637
    },
 
1638
    build_eval_context: function() {
 
1639
        return new instance.web.CompoundContext(this.eval_context);
 
1640
    },
 
1641
});
 
1642
 
 
1643
instance.web.form.compute_domain = function(expr, fields) {
 
1644
    if (! (expr instanceof Array))
 
1645
        return !! expr;
 
1646
    var stack = [];
 
1647
    for (var i = expr.length - 1; i >= 0; i--) {
 
1648
        var ex = expr[i];
 
1649
        if (ex.length == 1) {
 
1650
            var top = stack.pop();
 
1651
            switch (ex) {
 
1652
                case '|':
 
1653
                    stack.push(stack.pop() || top);
 
1654
                    continue;
 
1655
                case '&':
 
1656
                    stack.push(stack.pop() && top);
 
1657
                    continue;
 
1658
                case '!':
 
1659
                    stack.push(!top);
 
1660
                    continue;
 
1661
                default:
 
1662
                    throw new Error(_.str.sprintf(
 
1663
                        _t("Unknown operator %s in domain %s"),
 
1664
                        ex, JSON.stringify(expr)));
 
1665
            }
 
1666
        }
 
1667
 
 
1668
        var field = fields[ex[0]];
 
1669
        if (!field) {
 
1670
            throw new Error(_.str.sprintf(
 
1671
                _t("Unknown field %s in domain %s"),
 
1672
                ex[0], JSON.stringify(expr)));
 
1673
        }
 
1674
        var field_value = field.get_value ? field.get_value() : field.value;
 
1675
        var op = ex[1];
 
1676
        var val = ex[2];
 
1677
 
 
1678
        switch (op.toLowerCase()) {
 
1679
            case '=':
 
1680
            case '==':
 
1681
                stack.push(_.isEqual(field_value, val));
 
1682
                break;
 
1683
            case '!=':
 
1684
            case '<>':
 
1685
                stack.push(!_.isEqual(field_value, val));
 
1686
                break;
 
1687
            case '<':
 
1688
                stack.push(field_value < val);
 
1689
                break;
 
1690
            case '>':
 
1691
                stack.push(field_value > val);
 
1692
                break;
 
1693
            case '<=':
 
1694
                stack.push(field_value <= val);
 
1695
                break;
 
1696
            case '>=':
 
1697
                stack.push(field_value >= val);
 
1698
                break;
 
1699
            case 'in':
 
1700
                if (!_.isArray(val)) val = [val];
 
1701
                stack.push(_(val).contains(field_value));
 
1702
                break;
 
1703
            case 'not in':
 
1704
                if (!_.isArray(val)) val = [val];
 
1705
                stack.push(!_(val).contains(field_value));
 
1706
                break;
 
1707
            default:
 
1708
                console.warn(
 
1709
                    _t("Unsupported operator %s in domain %s"),
 
1710
                    op, JSON.stringify(expr));
 
1711
        }
 
1712
    }
 
1713
    return _.all(stack, _.identity);
 
1714
};
 
1715
 
 
1716
instance.web.form.is_bin_size = function(v) {
 
1717
    return /^\d+(\.\d*)? \w+$/.test(v);
 
1718
};
 
1719
 
 
1720
/**
 
1721
 * Must be applied over an class already possessing the PropertiesMixin.
 
1722
 *
 
1723
 * Apply the result of the "invisible" domain to this.$el.
 
1724
 */
 
1725
instance.web.form.InvisibilityChangerMixin = {
 
1726
    init: function(field_manager, invisible_domain) {
 
1727
        var self = this;
 
1728
        this._ic_field_manager = field_manager;
 
1729
        this._ic_invisible_modifier = invisible_domain;
 
1730
        this._ic_field_manager.on("view_content_has_changed", this, function() {
 
1731
            var result = self._ic_invisible_modifier === undefined ? false :
 
1732
                self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
 
1733
            self.set({"invisible": result});
 
1734
        });
 
1735
        this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
 
1736
        var check = function() {
 
1737
            if (self.get("invisible") || self.get('force_invisible')) {
 
1738
                self.set({"effective_invisible": true});
 
1739
            } else {
 
1740
                self.set({"effective_invisible": false});
 
1741
            }
 
1742
        };
 
1743
        this.on('change:invisible', this, check);
 
1744
        this.on('change:force_invisible', this, check);
 
1745
        check.call(this);
 
1746
    },
 
1747
    start: function() {
 
1748
        this.on("change:effective_invisible", this, this._check_visibility);
 
1749
        this._check_visibility();
 
1750
    },
 
1751
    _check_visibility: function() {
 
1752
        this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible"));
 
1753
    },
 
1754
};
 
1755
 
 
1756
instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, {
 
1757
    init: function(parent, field_manager, invisible_domain, $el) {
 
1758
        this.setParent(parent);
 
1759
        instance.web.PropertiesMixin.init.call(this);
 
1760
        instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
 
1761
        this.$el = $el;
 
1762
        this.start();
 
1763
    },
 
1764
});
 
1765
 
 
1766
/**
 
1767
    Base class for all fields, custom widgets and buttons to be displayed in the form view.
 
1768
 
 
1769
    Properties:
 
1770
        - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
 
1771
        the values of the "readonly" property and the "mode" property on the field manager.
 
1772
*/
 
1773
instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, {
 
1774
    /**
 
1775
     * @constructs instance.web.form.FormWidget
 
1776
     * @extends instance.web.Widget
 
1777
     *
 
1778
     * @param field_manager
 
1779
     * @param node
 
1780
     */
 
1781
    init: function(field_manager, node) {
 
1782
        this._super(field_manager);
 
1783
        this.field_manager = field_manager;
 
1784
        if (this.field_manager instanceof instance.web.FormView)
 
1785
            this.view = this.field_manager;
 
1786
        this.node = node;
 
1787
        this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
 
1788
        instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);
 
1789
 
 
1790
        this.field_manager.on("view_content_has_changed", this, this.process_modifiers);
 
1791
 
 
1792
        this.set({
 
1793
            required: false,
 
1794
            readonly: false,
 
1795
        });
 
1796
        // some events to make the property "effective_readonly" sync automatically with "readonly" and
 
1797
        // "mode" on field_manager
 
1798
        var self = this;
 
1799
        var test_effective_readonly = function() {
 
1800
            self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
 
1801
        };
 
1802
        this.on("change:readonly", this, test_effective_readonly);
 
1803
        this.field_manager.on("change:actual_mode", this, test_effective_readonly);
 
1804
        test_effective_readonly.call(this);
 
1805
    },
 
1806
    renderElement: function() {
 
1807
        this.process_modifiers();
 
1808
        this._super();
 
1809
        this.$el.addClass(this.node.attrs["class"] || "");
 
1810
    },
 
1811
    destroy: function() {
 
1812
        $.fn.tipsy.clear();
 
1813
        this._super.apply(this, arguments);
 
1814
    },
 
1815
    /**
 
1816
     * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
 
1817
     *
 
1818
     * This method is an utility method that is meant to be called by child classes.
 
1819
     *
 
1820
     * @param {jQuery} $e jQuery object of elements to bind focus/blur on
 
1821
     */
 
1822
    setupFocus: function ($e) {
 
1823
        var self = this;
 
1824
        $e.on({
 
1825
            focus: function () { self.trigger('focused'); },
 
1826
            blur: function () { self.trigger('blurred'); }
 
1827
        });
 
1828
    },
 
1829
    process_modifiers: function() {
 
1830
        var to_set = {};
 
1831
        for (var a in this.modifiers) {
 
1832
            if (!this.modifiers.hasOwnProperty(a)) { continue; }
 
1833
            if (!_.include(["invisible"], a)) {
 
1834
                var val = this.field_manager.compute_domain(this.modifiers[a]);
 
1835
                to_set[a] = val;
 
1836
            }
 
1837
        }
 
1838
        this.set(to_set);
 
1839
    },
 
1840
    do_attach_tooltip: function(widget, trigger, options) {
 
1841
        widget = widget || this;
 
1842
        trigger = trigger || this.$el;
 
1843
        options = _.extend({
 
1844
                delayIn: 500,
 
1845
                delayOut: 0,
 
1846
                fade: true,
 
1847
                title: function() {
 
1848
                    var template = widget.template + '.tooltip';
 
1849
                    if (!QWeb.has_template(template)) {
 
1850
                        template = 'WidgetLabel.tooltip';
 
1851
                    }
 
1852
                    return QWeb.render(template, {
 
1853
                        debug: instance.session.debug,
 
1854
                        widget: widget
 
1855
                })},
 
1856
                gravity: $.fn.tipsy.autoBounds(50, 'nw'),
 
1857
                html: true,
 
1858
                opacity: 0.85,
 
1859
                trigger: 'hover'
 
1860
            }, options || {});
 
1861
        $(trigger).tipsy(options);
 
1862
    },
 
1863
    /**
 
1864
     * Builds a new context usable for operations related to fields by merging
 
1865
     * the fields'context with the action's context.
 
1866
     */
 
1867
    build_context: function() {
 
1868
        // only use the model's context if there is not context on the node
 
1869
        var v_context = this.node.attrs.context;
 
1870
        if (! v_context) {
 
1871
            v_context = (this.field || {}).context || {};
 
1872
        }
 
1873
 
 
1874
        if (v_context.__ref || true) { //TODO: remove true
 
1875
            var fields_values = this.field_manager.build_eval_context();
 
1876
            v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values);
 
1877
        }
 
1878
        return v_context;
 
1879
    },
 
1880
    build_domain: function() {
 
1881
        var f_domain = this.field.domain || [];
 
1882
        var n_domain = this.node.attrs.domain || null;
 
1883
        // if there is a domain on the node, overrides the model's domain
 
1884
        var final_domain = n_domain !== null ? n_domain : f_domain;
 
1885
        if (!(final_domain instanceof Array) || true) { //TODO: remove true
 
1886
            var fields_values = this.field_manager.build_eval_context();
 
1887
            final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values);
 
1888
        }
 
1889
        return final_domain;
 
1890
    }
 
1891
});
 
1892
 
 
1893
instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
 
1894
    template: 'WidgetButton',
 
1895
    init: function(field_manager, node) {
 
1896
        node.attrs.type = node.attrs['data-button-type'];
 
1897
        this._super(field_manager, node);
 
1898
        this.force_disabled = false;
 
1899
        this.string = (this.node.attrs.string || '').replace(/_/g, '');
 
1900
        if (JSON.parse(this.node.attrs.default_focus || "0")) {
 
1901
            // TODO fme: provide enter key binding to widgets
 
1902
            this.view.default_focus_button = this;
 
1903
        }
 
1904
        if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) {
 
1905
            this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png';
 
1906
        }
 
1907
    },
 
1908
    start: function() {
 
1909
        this._super.apply(this, arguments);
 
1910
        this.view.on('view_content_has_changed', this, this.check_disable);
 
1911
        this.check_disable();
 
1912
        this.$el.click(this.on_click);
 
1913
        if (this.node.attrs.help || instance.session.debug) {
 
1914
            this.do_attach_tooltip();
 
1915
        }
 
1916
        this.setupFocus(this.$el);
 
1917
    },
 
1918
    on_click: function() {
 
1919
        var self = this;
 
1920
        this.force_disabled = true;
 
1921
        this.check_disable();
 
1922
        this.execute_action().always(function() {
 
1923
            self.force_disabled = false;
 
1924
            self.check_disable();
 
1925
        });
 
1926
    },
 
1927
    execute_action: function() {
 
1928
        var self = this;
 
1929
        var exec_action = function() {
 
1930
            if (self.node.attrs.confirm) {
 
1931
                var def = $.Deferred();
 
1932
                var dialog = instance.web.dialog($('<div/>').text(self.node.attrs.confirm), {
 
1933
                    title: _t('Confirm'),
 
1934
                    modal: true,
 
1935
                    buttons: [
 
1936
                        {text: _t("Cancel"), click: function() {
 
1937
                                $(this).dialog("close");
 
1938
                            }
 
1939
                        },
 
1940
                        {text: _t("Ok"), click: function() {
 
1941
                                var self2 = this;
 
1942
                                self.on_confirmed().always(function() {
 
1943
                                    $(self2).dialog("close");
 
1944
                                });
 
1945
                            }
 
1946
                        }
 
1947
                    ],
 
1948
                    beforeClose: function() {
 
1949
                        def.resolve();
 
1950
                    },
 
1951
                });
 
1952
                return def.promise();
 
1953
            } else {
 
1954
                return self.on_confirmed();
 
1955
            }
 
1956
        };
 
1957
        if (!this.node.attrs.special) {
 
1958
            return this.view.recursive_save().then(exec_action);
 
1959
        } else {
 
1960
            return exec_action();
 
1961
        }
 
1962
    },
 
1963
    on_confirmed: function() {
 
1964
        var self = this;
 
1965
 
 
1966
        var context = this.build_context();
 
1967
 
 
1968
        return this.view.do_execute_action(
 
1969
            _.extend({}, this.node.attrs, {context: context}),
 
1970
            this.view.dataset, this.view.datarecord.id, function () {
 
1971
                self.view.recursive_reload();
 
1972
            });
 
1973
    },
 
1974
    check_disable: function() {
 
1975
        var disabled = (this.force_disabled || !this.view.is_interactible_record());
 
1976
        this.$el.prop('disabled', disabled);
 
1977
        this.$el.css('color', disabled ? 'grey' : '');
 
1978
    }
 
1979
});
 
1980
 
 
1981
/**
 
1982
 * Interface to be implemented by fields.
 
1983
 *
 
1984
 * Events:
 
1985
 *     - changed_value: triggered when the value of the field has changed. This can be due
 
1986
 *      to a user interaction or a call to set_value().
 
1987
 *
 
1988
 */
 
1989
instance.web.form.FieldInterface = {
 
1990
    /**
 
1991
     * Constructor takes 2 arguments:
 
1992
     * - field_manager: Implements FieldManagerMixin
 
1993
     * - node: the "<field>" node in json form
 
1994
     */
 
1995
    init: function(field_manager, node) {},
 
1996
    /**
 
1997
     * Called by the form view to indicate the value of the field.
 
1998
     *
 
1999
     * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
 
2000
     * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
 
2001
     * before the widget is inserted into the DOM.
 
2002
     *
 
2003
     * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
 
2004
     * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must
 
2005
     * also be able to handle any other format commonly used in the _defaults key on the models in the addons
 
2006
     * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
 
2007
     * no information is ever given to know which format is used.
 
2008
     */
 
2009
    set_value: function(value_) {},
 
2010
    /**
 
2011
     * Get the current value of the widget.
 
2012
     *
 
2013
     * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
 
2014
     * the OpenERP server, although it is not assumed to respect the constraints applied to the field.
 
2015
     * For example if the field is marked as "required", a call to get_value() can return false.
 
2016
     *
 
2017
     * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
 
2018
     * return a default value according to the type of field.
 
2019
     *
 
2020
     * This method is always assumed to perform synchronously, it can not return a promise.
 
2021
     *
 
2022
     * If there was no user interaction to modify the value of the field, it is always assumed that
 
2023
     * get_value() return the same semantic value than the one passed in the last call to set_value(),
 
2024
     * although the syntax can be different. This can be the case for type of fields that have a different
 
2025
     * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
 
2026
     */
 
2027
    get_value: function() {},
 
2028
    /**
 
2029
     * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
 
2030
     * view.
 
2031
     */
 
2032
    set_input_id: function(id) {},
 
2033
    /**
 
2034
     * Returns true if is_syntax_valid() returns true and the value is semantically
 
2035
     * valid too according to the semantic restrictions applied to the field.
 
2036
     */
 
2037
    is_valid: function() {},
 
2038
    /**
 
2039
     * Returns true if the field holds a value which is syntactically correct, ignoring
 
2040
     * the potential semantic restrictions applied to the field.
 
2041
     */
 
2042
    is_syntax_valid: function() {},
 
2043
    /**
 
2044
     * Must set the focus on the field. Return false if field is not focusable.
 
2045
     */
 
2046
    focus: function() {},
 
2047
    /**
 
2048
     * Called when the translate button is clicked.
 
2049
     */
 
2050
    on_translate: function() {},
 
2051
    /**
 
2052
        This method is called by the form view before reading on_change values and before saving. It tells
 
2053
        the field to save its value before reading it using get_value(). Must return a promise.
 
2054
    */
 
2055
    commit_value: function() {},
 
2056
};
 
2057
 
 
2058
/**
 
2059
 * Abstract class for classes implementing FieldInterface.
 
2060
 *
 
2061
 * Properties:
 
2062
 *     - value: useful property to hold the value of the field. By default, set_value() and get_value()
 
2063
 *     set and retrieve the value property. Changing the value property also triggers automatically
 
2064
 *     a 'changed_value' event that inform the view to trigger on_changes.
 
2065
 *
 
2066
 */
 
2067
instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, {
 
2068
    /**
 
2069
     * @constructs instance.web.form.AbstractField
 
2070
     * @extends instance.web.form.FormWidget
 
2071
     *
 
2072
     * @param field_manager
 
2073
     * @param node
 
2074
     */
 
2075
    init: function(field_manager, node) {
 
2076
        var self = this
 
2077
        this._super(field_manager, node);
 
2078
        this.name = this.node.attrs.name;
 
2079
        this.field = this.field_manager.get_field_desc(this.name);
 
2080
        this.widget = this.node.attrs.widget;
 
2081
        this.string = this.node.attrs.string || this.field.string || this.name;
 
2082
        this.options = instance.web.py_eval(this.node.attrs.options || '{}');
 
2083
        this.set({'value': false});
 
2084
 
 
2085
        this.on("change:value", this, function() {
 
2086
            this.trigger('changed_value');
 
2087
            this._check_css_flags();
 
2088
        });
 
2089
    },
 
2090
    renderElement: function() {
 
2091
        var self = this;
 
2092
        this._super();
 
2093
        if (this.field.translate && this.view) {
 
2094
            this.$el.addClass('oe_form_field_translatable');
 
2095
            this.$el.find('.oe_field_translate').click(this.on_translate);
 
2096
        }
 
2097
        this.$label = this.view ? this.view.$el.find('label[for=' + this.id_for_label + ']') : $();
 
2098
        if (instance.session.debug) {
 
2099
            this.do_attach_tooltip(this, this.$label[0] || this.$el);
 
2100
            this.$label.off('dblclick').on('dblclick', function() {
 
2101
                console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
 
2102
                window.w = self;
 
2103
                console.log("window.w =", window.w);
 
2104
            });
 
2105
        }
 
2106
        if (!this.disable_utility_classes) {
 
2107
            this.off("change:required", this, this._set_required);
 
2108
            this.on("change:required", this, this._set_required);
 
2109
            this._set_required();
 
2110
        }
 
2111
        this._check_visibility();
 
2112
        this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
 
2113
        this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
 
2114
        this._check_css_flags();
 
2115
    },
 
2116
    start: function() {
 
2117
        var tmp = this._super();
 
2118
        this.on("change:value", this, function() {
 
2119
            if (! this.no_rerender)
 
2120
                this.render_value();
 
2121
        });
 
2122
        this.render_value();
 
2123
    },
 
2124
    /**
 
2125
     * Private. Do not use.
 
2126
     */
 
2127
    _set_required: function() {
 
2128
        this.$el.toggleClass('oe_form_required', this.get("required"));
 
2129
    },
 
2130
    set_value: function(value_) {
 
2131
        this.set({'value': value_});
 
2132
    },
 
2133
    get_value: function() {
 
2134
        return this.get('value');
 
2135
    },
 
2136
    /**
 
2137
        Utility method that all implementations should use to change the
 
2138
        value without triggering a re-rendering.
 
2139
    */
 
2140
    internal_set_value: function(value_) {
 
2141
        var tmp = this.no_rerender;
 
2142
        this.no_rerender = true;
 
2143
        this.set({'value': value_});
 
2144
        this.no_rerender = tmp;
 
2145
    },
 
2146
    /**
 
2147
        This method is called each time the value is modified.
 
2148
    */
 
2149
    render_value: function() {},
 
2150
    is_valid: function() {
 
2151
        return this.is_syntax_valid() && !(this.get('required') && this.is_false());
 
2152
    },
 
2153
    is_syntax_valid: function() {
 
2154
        return true;
 
2155
    },
 
2156
    /**
 
2157
     * Method useful to implement to ease validity testing. Must return true if the current
 
2158
     * value is similar to false in OpenERP.
 
2159
     */
 
2160
    is_false: function() {
 
2161
        return this.get('value') === false;
 
2162
    },
 
2163
    _check_css_flags: function() {
 
2164
        if (this.field.translate) {
 
2165
            this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create");
 
2166
        }
 
2167
        if (!this.disable_utility_classes) {
 
2168
            if (this.field_manager.get('display_invalid_fields')) {
 
2169
                this.$el.toggleClass('oe_form_invalid', !this.is_valid());
 
2170
            }
 
2171
        }
 
2172
    },
 
2173
    focus: function() {
 
2174
        return false;
 
2175
    },
 
2176
    set_input_id: function(id) {
 
2177
        this.id_for_label = id;
 
2178
    },
 
2179
    on_translate: function() {
 
2180
        var self = this;
 
2181
        var trans = new instance.web.DataSet(this, 'ir.translation');
 
2182
        return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
 
2183
            self.do_action(r);
 
2184
        });
 
2185
    },
 
2186
 
 
2187
    set_dimensions: function (height, width) {
 
2188
        this.$el.css({
 
2189
            width: width,
 
2190
            minHeight: height
 
2191
        });
 
2192
    },
 
2193
    commit_value: function() {
 
2194
        return $.when();
 
2195
    },
 
2196
});
 
2197
 
 
2198
/**
 
2199
 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
 
2200
 * switch.
 
2201
 */
 
2202
instance.web.form.ReinitializeWidgetMixin =  {
 
2203
    /**
 
2204
     * Default implementation of, you should not override it, use initialize_field() instead.
 
2205
     */
 
2206
    start: function() {
 
2207
        this.initialize_field();
 
2208
        this._super();
 
2209
    },
 
2210
    initialize_field: function() {
 
2211
        this.on("change:effective_readonly", this, this.reinitialize);
 
2212
        this.initialize_content();
 
2213
    },
 
2214
    reinitialize: function() {
 
2215
        this.destroy_content();
 
2216
        this.renderElement();
 
2217
        this.initialize_content();
 
2218
    },
 
2219
    /**
 
2220
     * Called to destroy anything that could have been created previously, called before a
 
2221
     * re-initialization.
 
2222
     */
 
2223
    destroy_content: function() {},
 
2224
    /**
 
2225
     * Called to initialize the content.
 
2226
     */
 
2227
    initialize_content: function() {},
 
2228
};
 
2229
 
 
2230
/**
 
2231
 * A mixin to apply on any field that has to completely re-render when its readonly state
 
2232
 * switch.
 
2233
 */
 
2234
instance.web.form.ReinitializeFieldMixin =  _.extend({}, instance.web.form.ReinitializeWidgetMixin, {
 
2235
    reinitialize: function() {
 
2236
        instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this);
 
2237
        this.render_value();
 
2238
    },
 
2239
});
 
2240
 
 
2241
/**
 
2242
    Some hack to make placeholders work in ie9.
 
2243
*/
 
2244
if ($.browser.msie && $.browser.version === "9.0") {
 
2245
    document.addEventListener("DOMNodeInserted",function(event){
 
2246
        var nodename =  event.target.nodeName.toLowerCase();
 
2247
        if ( nodename === "input" || nodename == "textarea" ) {
 
2248
            $(event.target).placeholder();
 
2249
        }
 
2250
    });
 
2251
}
 
2252
 
 
2253
instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 
2254
    template: 'FieldChar',
 
2255
    widget_class: 'oe_form_field_char',
 
2256
    events: {
 
2257
        'change input': 'store_dom_value',
 
2258
    },
 
2259
    init: function (field_manager, node) {
 
2260
        this._super(field_manager, node);
 
2261
        this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1';
 
2262
    },
 
2263
    initialize_content: function() {
 
2264
        this.setupFocus(this.$('input'));
 
2265
    },
 
2266
    store_dom_value: function () {
 
2267
        if (!this.get('effective_readonly')
 
2268
                && this.$('input').length
 
2269
                && this.is_syntax_valid()) {
 
2270
            this.internal_set_value(
 
2271
                this.parse_value(
 
2272
                    this.$('input').val()));
 
2273
        }
 
2274
    },
 
2275
    commit_value: function () {
 
2276
        this.store_dom_value();
 
2277
        return this._super();
 
2278
    },
 
2279
    render_value: function() {
 
2280
        var show_value = this.format_value(this.get('value'), '');
 
2281
        if (!this.get("effective_readonly")) {
 
2282
            this.$el.find('input').val(show_value);
 
2283
        } else {
 
2284
            if (this.password) {
 
2285
                show_value = new Array(show_value.length + 1).join('*');
 
2286
            }
 
2287
            this.$(".oe_form_char_content").text(show_value);
 
2288
        }
 
2289
    },
 
2290
    is_syntax_valid: function() {
 
2291
        if (!this.get("effective_readonly") && this.$("input").size() > 0) {
 
2292
            try {
 
2293
                this.parse_value(this.$('input').val(), '');
 
2294
                return true;
 
2295
            } catch(e) {
 
2296
                return false;
 
2297
            }
 
2298
        }
 
2299
        return true;
 
2300
    },
 
2301
    parse_value: function(val, def) {
 
2302
        return instance.web.parse_value(val, this, def);
 
2303
    },
 
2304
    format_value: function(val, def) {
 
2305
        return instance.web.format_value(val, this, def);
 
2306
    },
 
2307
    is_false: function() {
 
2308
        return this.get('value') === '' || this._super();
 
2309
    },
 
2310
    focus: function() {
 
2311
        var input = this.$('input:first')[0];
 
2312
        return input ? input.focus() : false;
 
2313
    },
 
2314
    set_dimensions: function (height, width) {
 
2315
        this._super(height, width);
 
2316
        this.$('input').css({
 
2317
            height: height,
 
2318
            width: width
 
2319
        });
 
2320
    }
 
2321
});
 
2322
 
 
2323
instance.web.form.FieldID = instance.web.form.FieldChar.extend({
 
2324
    process_modifiers: function () {
 
2325
        this._super();
 
2326
        this.set({ readonly: true });
 
2327
    },
 
2328
});
 
2329
 
 
2330
instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({
 
2331
    template: 'FieldEmail',
 
2332
    initialize_content: function() {
 
2333
        this._super();
 
2334
        var $button = this.$el.find('button');
 
2335
        $button.click(this.on_button_clicked);
 
2336
        this.setupFocus($button);
 
2337
    },
 
2338
    render_value: function() {
 
2339
        if (!this.get("effective_readonly")) {
 
2340
            this._super();
 
2341
        } else {
 
2342
            this.$el.find('a')
 
2343
                    .attr('href', 'mailto:' + this.get('value'))
 
2344
                    .text(this.get('value') || '');
 
2345
        }
 
2346
    },
 
2347
    on_button_clicked: function() {
 
2348
        if (!this.get('value') || !this.is_syntax_valid()) {
 
2349
            this.do_warn(_t("E-mail Error"), _t("Can't send email to invalid e-mail address"));
 
2350
        } else {
 
2351
            location.href = 'mailto:' + this.get('value');
 
2352
        }
 
2353
    }
 
2354
});
 
2355
 
 
2356
instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({
 
2357
    template: 'FieldUrl',
 
2358
    initialize_content: function() {
 
2359
        this._super();
 
2360
        var $button = this.$el.find('button');
 
2361
        $button.click(this.on_button_clicked);
 
2362
        this.setupFocus($button);
 
2363
    },
 
2364
    render_value: function() {
 
2365
        if (!this.get("effective_readonly")) {
 
2366
            this._super();
 
2367
        } else {
 
2368
            var tmp = this.get('value');
 
2369
            var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp);
 
2370
            if (!s) {
 
2371
                tmp = "http://" + this.get('value');
 
2372
            }
 
2373
            var text = this.get('value') ? this.node.attrs.text || tmp : '';
 
2374
            this.$el.find('a').attr('href', tmp).text(text);
 
2375
        }
 
2376
    },
 
2377
    on_button_clicked: function() {
 
2378
        if (!this.get('value')) {
 
2379
            this.do_warn(_t("Resource Error"), _t("This resource is empty"));
 
2380
        } else {
 
2381
            var url = $.trim(this.get('value'));
 
2382
            if(/^www\./i.test(url))
 
2383
                url = 'http://'+url;
 
2384
            window.open(url);
 
2385
        }
 
2386
    }
 
2387
});
 
2388
 
 
2389
instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({
 
2390
    is_field_number: true,
 
2391
    widget_class: 'oe_form_field_float',
 
2392
    init: function (field_manager, node) {
 
2393
        this._super(field_manager, node);
 
2394
        this.internal_set_value(0);
 
2395
        if (this.node.attrs.digits) {
 
2396
            this.digits = this.node.attrs.digits;
 
2397
        } else {
 
2398
            this.digits = this.field.digits;
 
2399
        }
 
2400
    },
 
2401
    set_value: function(value_) {
 
2402
        if (value_ === false || value_ === undefined) {
 
2403
            // As in GTK client, floats default to 0
 
2404
            value_ = 0;
 
2405
        }
 
2406
        this._super.apply(this, [value_]);
 
2407
    },
 
2408
    focus: function () {
 
2409
        var $input = this.$('input:first');
 
2410
        return $input.length ? $input.select() : false;
 
2411
    }
 
2412
});
 
2413
 
 
2414
instance.web.DateTimeWidget = instance.web.Widget.extend({
 
2415
    template: "web.datepicker",
 
2416
    jqueryui_object: 'datetimepicker',
 
2417
    type_of_date: "datetime",
 
2418
    events: {
 
2419
        'change .oe_datepicker_master': 'change_datetime',
 
2420
    },
 
2421
    init: function(parent) {
 
2422
        this._super(parent);
 
2423
        this.name = parent.name;
 
2424
    },
 
2425
    start: function() {
 
2426
        var self = this;
 
2427
        this.$input = this.$el.find('input.oe_datepicker_master');
 
2428
        this.$input_picker = this.$el.find('input.oe_datepicker_container');
 
2429
 
 
2430
        $.datepicker.setDefaults({
 
2431
            clearText: _t('Clear'),
 
2432
            clearStatus: _t('Erase the current date'),
 
2433
            closeText: _t('Done'),
 
2434
            closeStatus: _t('Close without change'),
 
2435
            prevText: _t('<Prev'),
 
2436
            prevStatus: _t('Show the previous month'),
 
2437
            nextText: _t('Next>'),
 
2438
            nextStatus: _t('Show the next month'),
 
2439
            currentText: _t('Today'),
 
2440
            currentStatus: _t('Show the current month'),
 
2441
            monthNames: Date.CultureInfo.monthNames,
 
2442
            monthNamesShort: Date.CultureInfo.abbreviatedMonthNames,
 
2443
            monthStatus: _t('Show a different month'),
 
2444
            yearStatus: _t('Show a different year'),
 
2445
            weekHeader: _t('Wk'),
 
2446
            weekStatus: _t('Week of the year'),
 
2447
            dayNames: Date.CultureInfo.dayNames,
 
2448
            dayNamesShort: Date.CultureInfo.abbreviatedDayNames,
 
2449
            dayNamesMin: Date.CultureInfo.shortestDayNames,
 
2450
            dayStatus: _t('Set DD as first week day'),
 
2451
            dateStatus: _t('Select D, M d'),
 
2452
            firstDay: Date.CultureInfo.firstDayOfWeek,
 
2453
            initStatus: _t('Select a date'),
 
2454
            isRTL: false
 
2455
        });
 
2456
        $.timepicker.setDefaults({
 
2457
            timeOnlyTitle: _t('Choose Time'),
 
2458
            timeText: _t('Time'),
 
2459
            hourText: _t('Hour'),
 
2460
            minuteText: _t('Minute'),
 
2461
            secondText: _t('Second'),
 
2462
            currentText: _t('Now'),
 
2463
            closeText: _t('Done')
 
2464
        });
 
2465
 
 
2466
        this.picker({
 
2467
            onClose: this.on_picker_select,
 
2468
            onSelect: this.on_picker_select,
 
2469
            changeMonth: true,
 
2470
            changeYear: true,
 
2471
            showWeek: true,
 
2472
            showButtonPanel: true,
 
2473
            firstDay: Date.CultureInfo.firstDayOfWeek
 
2474
        });
 
2475
        // Some clicks in the datepicker dialog are not stopped by the
 
2476
        // datepicker and "bubble through", unexpectedly triggering the bus's
 
2477
        // click event. Prevent that.
 
2478
        this.picker('widget').click(function (e) { e.stopPropagation(); });
 
2479
 
 
2480
        this.$el.find('img.oe_datepicker_trigger').click(function() {
 
2481
            if (self.get("effective_readonly") || self.picker('widget').is(':visible')) {
 
2482
                self.$input.focus();
 
2483
                return;
 
2484
            }
 
2485
            self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
 
2486
            self.$input_picker.show();
 
2487
            self.picker('show');
 
2488
            self.$input_picker.hide();
 
2489
        });
 
2490
        this.set_readonly(false);
 
2491
        this.set({'value': false});
 
2492
    },
 
2493
    picker: function() {
 
2494
        return $.fn[this.jqueryui_object].apply(this.$input_picker, arguments);
 
2495
    },
 
2496
    on_picker_select: function(text, instance_) {
 
2497
        var date = this.picker('getDate');
 
2498
        this.$input
 
2499
            .val(date ? this.format_client(date) : '')
 
2500
            .change()
 
2501
            .focus();
 
2502
    },
 
2503
    set_value: function(value_) {
 
2504
        this.set({'value': value_});
 
2505
        this.$input.val(value_ ? this.format_client(value_) : '');
 
2506
    },
 
2507
    get_value: function() {
 
2508
        return this.get('value');
 
2509
    },
 
2510
    set_value_from_ui_: function() {
 
2511
        var value_ = this.$input.val() || false;
 
2512
        this.set({'value': this.parse_client(value_)});
 
2513
    },
 
2514
    set_readonly: function(readonly) {
 
2515
        this.readonly = readonly;
 
2516
        this.$input.prop('readonly', this.readonly);
 
2517
        this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly);
 
2518
    },
 
2519
    is_valid_: function() {
 
2520
        var value_ = this.$input.val();
 
2521
        if (value_ === "") {
 
2522
            return true;
 
2523
        } else {
 
2524
            try {
 
2525
                this.parse_client(value_);
 
2526
                return true;
 
2527
            } catch(e) {
 
2528
                return false;
 
2529
            }
 
2530
        }
 
2531
    },
 
2532
    parse_client: function(v) {
 
2533
        return instance.web.parse_value(v, {"widget": this.type_of_date});
 
2534
    },
 
2535
    format_client: function(v) {
 
2536
        return instance.web.format_value(v, {"widget": this.type_of_date});
 
2537
    },
 
2538
    change_datetime: function() {
 
2539
        if (this.is_valid_()) {
 
2540
            this.set_value_from_ui_();
 
2541
            this.trigger("datetime_changed");
 
2542
        }
 
2543
    },
 
2544
    commit_value: function () {
 
2545
        this.change_datetime();
 
2546
    },
 
2547
});
 
2548
 
 
2549
instance.web.DateWidget = instance.web.DateTimeWidget.extend({
 
2550
    jqueryui_object: 'datepicker',
 
2551
    type_of_date: "date"
 
2552
});
 
2553
 
 
2554
instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 
2555
    template: "FieldDatetime",
 
2556
    build_widget: function() {
 
2557
        return new instance.web.DateTimeWidget(this);
 
2558
    },
 
2559
    destroy_content: function() {
 
2560
        if (this.datewidget) {
 
2561
            this.datewidget.destroy();
 
2562
            this.datewidget = undefined;
 
2563
        }
 
2564
    },
 
2565
    initialize_content: function() {
 
2566
        if (!this.get("effective_readonly")) {
 
2567
            this.datewidget = this.build_widget();
 
2568
            this.datewidget.on('datetime_changed', this, _.bind(function() {
 
2569
                this.internal_set_value(this.datewidget.get_value());
 
2570
            }, this));
 
2571
            this.datewidget.appendTo(this.$el);
 
2572
            this.setupFocus(this.datewidget.$input);
 
2573
        }
 
2574
    },
 
2575
    render_value: function() {
 
2576
        if (!this.get("effective_readonly")) {
 
2577
            this.datewidget.set_value(this.get('value'));
 
2578
        } else {
 
2579
            this.$el.text(instance.web.format_value(this.get('value'), this, ''));
 
2580
        }
 
2581
    },
 
2582
    is_syntax_valid: function() {
 
2583
        if (!this.get("effective_readonly") && this.datewidget) {
 
2584
            return this.datewidget.is_valid_();
 
2585
        }
 
2586
        return true;
 
2587
    },
 
2588
    is_false: function() {
 
2589
        return this.get('value') === '' || this._super();
 
2590
    },
 
2591
    focus: function() {
 
2592
        var input = this.datewidget && this.datewidget.$input[0];
 
2593
        return input ? input.focus() : false;
 
2594
    },
 
2595
    set_dimensions: function (height, width) {
 
2596
        this._super(height, width);
 
2597
        this.datewidget.$input.css('height', height);
 
2598
    }
 
2599
});
 
2600
 
 
2601
instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({
 
2602
    template: "FieldDate",
 
2603
    build_widget: function() {
 
2604
        return new instance.web.DateWidget(this);
 
2605
    }
 
2606
});
 
2607
 
 
2608
instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 
2609
    template: 'FieldText',
 
2610
    events: {
 
2611
        'keyup': function (e) {
 
2612
            if (e.which === $.ui.keyCode.ENTER) {
 
2613
                e.stopPropagation();
 
2614
            }
 
2615
        },
 
2616
        'change textarea': 'store_dom_value',
 
2617
    },
 
2618
    initialize_content: function() {
 
2619
        var self = this;
 
2620
        if (! this.get("effective_readonly")) {
 
2621
            this.$textarea = this.$el.find('textarea');
 
2622
            this.auto_sized = false;
 
2623
            this.default_height = this.$textarea.css('height');
 
2624
            if (this.get("effective_readonly")) {
 
2625
                this.$textarea.attr('disabled', 'disabled');
 
2626
            }
 
2627
            this.setupFocus(this.$textarea);
 
2628
        } else {
 
2629
            this.$textarea = undefined;
 
2630
        }
 
2631
    },
 
2632
    commit_value: function () {
 
2633
        if (! this.get("effective_readonly") && this.$textarea) {
 
2634
            this.store_dom_value();
 
2635
        }
 
2636
        return this._super();
 
2637
    },
 
2638
    store_dom_value: function () {
 
2639
        this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this));
 
2640
    },
 
2641
    render_value: function() {
 
2642
        if (! this.get("effective_readonly")) {
 
2643
            var show_value = instance.web.format_value(this.get('value'), this, '');
 
2644
            if (show_value === '') {
 
2645
                this.$textarea.css('height', parseInt(this.default_height)+"px");
 
2646
            }
 
2647
            this.$textarea.val(show_value);
 
2648
            if (! this.auto_sized) {
 
2649
                this.auto_sized = true;
 
2650
                this.$textarea.autosize();
 
2651
            } else {
 
2652
                this.$textarea.trigger("autosize");
 
2653
            }
 
2654
        } else {
 
2655
            var txt = this.get("value") || '';
 
2656
            this.$(".oe_form_text_content").text(txt);
 
2657
        }
 
2658
    },
 
2659
    is_syntax_valid: function() {
 
2660
        if (!this.get("effective_readonly") && this.$textarea) {
 
2661
            try {
 
2662
                instance.web.parse_value(this.$textarea.val(), this, '');
 
2663
                return true;
 
2664
            } catch(e) {
 
2665
                return false;
 
2666
            }
 
2667
        }
 
2668
        return true;
 
2669
    },
 
2670
    is_false: function() {
 
2671
        return this.get('value') === '' || this._super();
 
2672
    },
 
2673
    focus: function($el) {
 
2674
        var input = !this.get("effective_readonly") && this.$textarea && this.$textarea[0];
 
2675
        return input ? input.focus() : false;
 
2676
    },
 
2677
    set_dimensions: function (height, width) {
 
2678
        this._super(height, width);
 
2679
        if (!this.get("effective_readonly") && this.$textarea) {
 
2680
            this.$textarea.css({
 
2681
                width: width,
 
2682
                minHeight: height
 
2683
            });
 
2684
        }
 
2685
    },
 
2686
});
 
2687
 
 
2688
/**
 
2689
 * FieldTextHtml Widget
 
2690
 * Intended for FieldText widgets meant to display HTML content. This
 
2691
 * widget will instantiate the CLEditor (see cleditor in static/src/lib)
 
2692
 * To find more information about CLEditor configutation: go to
 
2693
 * http://premiumsoftware.net/cleditor/docs/GettingStarted.html
 
2694
 */
 
2695
instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 
2696
    template: 'FieldTextHtml',
 
2697
    init: function() {
 
2698
        this._super.apply(this, arguments);
 
2699
    },
 
2700
    initialize_content: function() {
 
2701
        var self = this;
 
2702
        if (! this.get("effective_readonly")) {
 
2703
            self._updating_editor = false;
 
2704
            this.$textarea = this.$el.find('textarea');
 
2705
            var width = ((this.node.attrs || {}).editor_width || '100%');
 
2706
            var height = ((this.node.attrs || {}).editor_height || 250);
 
2707
            this.$textarea.cleditor({
 
2708
                width:      width, // width not including margins, borders or padding
 
2709
                height:     height, // height not including margins, borders or padding
 
2710
                controls:   // controls to add to the toolbar
 
2711
                            "bold italic underline strikethrough " +
 
2712
                            "| removeformat | bullets numbering | outdent " +
 
2713
                            "indent | link unlink | source",
 
2714
                bodyStyle:  // style to assign to document body contained within the editor
 
2715
                            "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text"
 
2716
            });
 
2717
            this.$cleditor = this.$textarea.cleditor()[0];
 
2718
            this.$cleditor.change(function() {
 
2719
                if (! self._updating_editor) {
 
2720
                    self.$cleditor.updateTextArea();
 
2721
                    self.internal_set_value(self.$textarea.val());
 
2722
                }
 
2723
            });
 
2724
            if (this.field.translate) {
 
2725
                var $img = $('<img class="oe_field_translate oe_input_icon" src="/web/static/src/img/icons/terp-translate.png" width="16" height="16" border="0"/>')
 
2726
                    .click(this.on_translate);
 
2727
                this.$cleditor.$toolbar.append($img);
 
2728
            }
 
2729
        }
 
2730
    },
 
2731
    render_value: function() {
 
2732
        if (! this.get("effective_readonly")) {
 
2733
            this.$textarea.val(this.get('value') || '');
 
2734
            this._updating_editor = true;
 
2735
            this.$cleditor.updateFrame();
 
2736
            this._updating_editor = false;
 
2737
        } else {
 
2738
            this.$el.html(this.get('value'));
 
2739
        }
 
2740
    },
 
2741
});
 
2742
 
 
2743
instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
 
2744
    template: 'FieldBoolean',
 
2745
    start: function() {
 
2746
        var self = this;
 
2747
        this.$checkbox = $("input", this.$el);
 
2748
        this.setupFocus(this.$checkbox);
 
2749
        this.$el.click(_.bind(function() {
 
2750
            this.internal_set_value(this.$checkbox.is(':checked'));
 
2751
        }, this));
 
2752
        var check_readonly = function() {
 
2753
            self.$checkbox.prop('disabled', self.get("effective_readonly"));
 
2754
        };
 
2755
        this.on("change:effective_readonly", this, check_readonly);
 
2756
        check_readonly.call(this);
 
2757
        this._super.apply(this, arguments);
 
2758
    },
 
2759
    render_value: function() {
 
2760
        this.$checkbox[0].checked = this.get('value');
 
2761
    },
 
2762
    focus: function() {
 
2763
        var input = this.$checkbox && this.$checkbox[0];
 
2764
        return input ? input.focus() : false;
 
2765
    }
 
2766
});
 
2767
 
 
2768
/**
 
2769
    The progressbar field expect a float from 0 to 100.
 
2770
*/
 
2771
instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
 
2772
    template: 'FieldProgressBar',
 
2773
    render_value: function() {
 
2774
        this.$el.progressbar({
 
2775
            value: this.get('value') || 0,
 
2776
            disabled: this.get("effective_readonly")
 
2777
        });
 
2778
        var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
 
2779
        this.$('span').html(formatted_value + '%');
 
2780
    }
 
2781
});
 
2782
 
 
2783
 
 
2784
instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 
2785
    template: 'FieldSelection',
 
2786
    events: {
 
2787
        'change select': 'store_dom_value',
 
2788
    },
 
2789
    init: function(field_manager, node) {
 
2790
        var self = this;
 
2791
        this._super(field_manager, node);
 
2792
        this.values = _(this.field.selection).chain()
 
2793
            .reject(function (v) { return v[0] === false && v[1] === ''; })
 
2794
            .unshift([false, ''])
 
2795
            .value();
 
2796
    },
 
2797
    initialize_content: function() {
 
2798
        // Flag indicating whether we're in an event chain containing a change
 
2799
        // event on the select, in order to know what to do on keyup[RETURN]:
 
2800
        // * If the user presses [RETURN] as part of changing the value of a
 
2801
        //   selection, we should just let the value change and not let the
 
2802
        //   event broadcast further (e.g. to validating the current state of
 
2803
        //   the form in editable list view, which would lead to saving the
 
2804
        //   current row or switching to the next one)
 
2805
        // * If the user presses [RETURN] with a select closed (side-effect:
 
2806
        //   also if the user opened the select and pressed [RETURN] without
 
2807
        //   changing the selected value), takes the action as validating the
 
2808
        //   row
 
2809
        var ischanging = false;
 
2810
        var $select = this.$el.find('select')
 
2811
            .change(function () { ischanging = true; })
 
2812
            .click(function () { ischanging = false; })
 
2813
            .keyup(function (e) {
 
2814
                if (e.which !== 13 || !ischanging) { return; }
 
2815
                e.stopPropagation();
 
2816
                ischanging = false;
 
2817
            });
 
2818
        this.setupFocus($select);
 
2819
    },
 
2820
    commit_value: function () {
 
2821
        this.store_dom_value();
 
2822
        return this._super();
 
2823
    },
 
2824
    store_dom_value: function () {
 
2825
        if (!this.get('effective_readonly') && this.$('select').length) {
 
2826
            this.internal_set_value(
 
2827
                this.values[this.$('select')[0].selectedIndex][0]);
 
2828
        }
 
2829
    },
 
2830
    set_value: function(value_) {
 
2831
        value_ = value_ === null ? false : value_;
 
2832
        value_ = value_ instanceof Array ? value_[0] : value_;
 
2833
        this._super(value_);
 
2834
    },
 
2835
    render_value: function() {
 
2836
        if (!this.get("effective_readonly")) {
 
2837
            var index = 0;
 
2838
            for (var i = 0, ii = this.values.length; i < ii; i++) {
 
2839
                if (this.values[i][0] === this.get('value')) index = i;
 
2840
            }
 
2841
            this.$el.find('select')[0].selectedIndex = index;
 
2842
        } else {
 
2843
            var self = this;
 
2844
            var option = _(this.values)
 
2845
                .detect(function (record) { return record[0] === self.get('value'); });
 
2846
            this.$el.text(option ? option[1] : this.values[0][1]);
 
2847
        }
 
2848
    },
 
2849
    focus: function() {
 
2850
        var input = this.$('select:first')[0];
 
2851
        return input ? input.focus() : false;
 
2852
    },
 
2853
    set_dimensions: function (height, width) {
 
2854
        this._super(height, width);
 
2855
        this.$('select').css({
 
2856
            height: height,
 
2857
            width: width
 
2858
        });
 
2859
    }
 
2860
});
 
2861
 
 
2862
// jquery autocomplete tweak to allow html and classnames
 
2863
(function() {
 
2864
    var proto = $.ui.autocomplete.prototype,
 
2865
        initSource = proto._initSource;
 
2866
 
 
2867
    function filter( array, term ) {
 
2868
        var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
 
2869
        return $.grep( array, function(value_) {
 
2870
            return matcher.test( $( "<div>" ).html( value_.label || value_.value || value_ ).text() );
 
2871
        });
 
2872
    }
 
2873
 
 
2874
    $.extend( proto, {
 
2875
        _initSource: function() {
 
2876
            if ( this.options.html && $.isArray(this.options.source) ) {
 
2877
                this.source = function( request, response ) {
 
2878
                    response( filter( this.options.source, request.term ) );
 
2879
                };
 
2880
            } else {
 
2881
                initSource.call( this );
 
2882
            }
 
2883
        },
 
2884
 
 
2885
        _renderItem: function( ul, item) {
 
2886
            return $( "<li></li>" )
 
2887
                .data( "item.autocomplete", item )
 
2888
                .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) )
 
2889
                .appendTo( ul )
 
2890
                .addClass(item.classname);
 
2891
        }
 
2892
    });
 
2893
})();
 
2894
 
 
2895
/**
 
2896
 * A mixin containing some useful methods to handle completion inputs.
 
2897
 */
 
2898
instance.web.form.CompletionFieldMixin = {
 
2899
    init: function() {
 
2900
        this.limit = 7;
 
2901
        this.orderer = new instance.web.DropMisordered();
 
2902
    },
 
2903
    /**
 
2904
     * Call this method to search using a string.
 
2905
     */
 
2906
    get_search_result: function(search_val) {
 
2907
        var self = this;
 
2908
 
 
2909
        var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context());
 
2910
        var blacklist = this.get_search_blacklist();
 
2911
        this.last_query = search_val;
 
2912
 
 
2913
        return this.orderer.add(dataset.name_search(
 
2914
                search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]),
 
2915
                'ilike', this.limit + 1, self.build_context())).then(function(data) {
 
2916
            self.last_search = data;
 
2917
            // possible selections for the m2o
 
2918
            var values = _.map(data, function(x) {
 
2919
                x[1] = x[1].split("\n")[0];
 
2920
                return {
 
2921
                    label: _.str.escapeHTML(x[1]),
 
2922
                    value: x[1],
 
2923
                    name: x[1],
 
2924
                    id: x[0],
 
2925
                };
 
2926
            });
 
2927
 
 
2928
            // search more... if more results that max
 
2929
            if (values.length > self.limit) {
 
2930
                values = values.slice(0, self.limit);
 
2931
                values.push({
 
2932
                    label: _t("Search More..."),
 
2933
                    action: function() {
 
2934
                        dataset.name_search(search_val, self.build_domain(), 'ilike', false).done(function(data) {
 
2935
                            self._search_create_popup("search", data);
 
2936
                        });
 
2937
                    },
 
2938
                    classname: 'oe_m2o_dropdown_option'
 
2939
                });
 
2940
            }
 
2941
            // quick create
 
2942
            var raw_result = _(data.result).map(function(x) {return x[1];});
 
2943
            if (search_val.length > 0 && !_.include(raw_result, search_val)) {
 
2944
                values.push({
 
2945
                    label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
 
2946
                        $('<span />').text(search_val).html()),
 
2947
                    action: function() {
 
2948
                        self._quick_create(search_val);
 
2949
                    },
 
2950
                    classname: 'oe_m2o_dropdown_option'
 
2951
                });
 
2952
            }
 
2953
            // create...
 
2954
            values.push({
 
2955
                label: _t("Create and Edit..."),
 
2956
                action: function() {
 
2957
                    self._search_create_popup("form", undefined, self._create_context(search_val));
 
2958
                },
 
2959
                classname: 'oe_m2o_dropdown_option'
 
2960
            });
 
2961
 
 
2962
            return values;
 
2963
        });
 
2964
    },
 
2965
    get_search_blacklist: function() {
 
2966
        return [];
 
2967
    },
 
2968
    _quick_create: function(name) {
 
2969
        var self = this;
 
2970
        var slow_create = function () {
 
2971
            self._search_create_popup("form", undefined, self._create_context(name));
 
2972
        };
 
2973
        if (self.options.quick_create === undefined || self.options.quick_create) {
 
2974
            new instance.web.DataSet(this, this.field.relation, self.build_context())
 
2975
                .name_create(name).done(function(data) {
 
2976
                    self.add_id(data[0]);
 
2977
                }).fail(function(error, event) {
 
2978
                    event.preventDefault();
 
2979
                    slow_create();
 
2980
                });
 
2981
        } else
 
2982
            slow_create();
 
2983
    },
 
2984
    // all search/create popup handling
 
2985
    _search_create_popup: function(view, ids, context) {
 
2986
        var self = this;
 
2987
        var pop = new instance.web.form.SelectCreatePopup(this);
 
2988
        pop.select_element(
 
2989
            self.field.relation,
 
2990
            {
 
2991
                title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
 
2992
                initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined,
 
2993
                initial_view: view,
 
2994
                disable_multiple_selection: true
 
2995
            },
 
2996
            self.build_domain(),
 
2997
            new instance.web.CompoundContext(self.build_context(), context || {})
 
2998
        );
 
2999
        pop.on("elements_selected", self, function(element_ids) {
 
3000
            self.add_id(element_ids[0]);
 
3001
            self.focus();
 
3002
        });
 
3003
    },
 
3004
    /**
 
3005
     * To implement.
 
3006
     */
 
3007
    add_id: function(id) {},
 
3008
    _create_context: function(name) {
 
3009
        var tmp = {};
 
3010
        var field = (this.options || {}).create_name_field;
 
3011
        if (field === undefined)
 
3012
            field = "name";
 
3013
        if (field !== false && name && (this.options || {}).quick_create !== false)
 
3014
            tmp["default_" + field] = name;
 
3015
        return tmp;
 
3016
    },
 
3017
};
 
3018
 
 
3019
instance.web.form.M2ODialog = instance.web.Dialog.extend({
 
3020
    template: "M2ODialog",
 
3021
    init: function(parent) {
 
3022
        this._super(parent, {
 
3023
            title: _.str.sprintf(_t("Add %s"), parent.string),
 
3024
            width: 312,
 
3025
        });
 
3026
    },
 
3027
    start: function() {
 
3028
        var self = this;
 
3029
        this.$buttons.html(QWeb.render("M2ODialog.buttons"));
 
3030
        this.$("input").val(this.getParent().last_query);
 
3031
        this.$buttons.find(".oe_form_m2o_qc_button").click(function(){
 
3032
            self.getParent()._quick_create(self.$("input").val());
 
3033
            self.destroy();
 
3034
        });
 
3035
        this.$buttons.find(".oe_form_m2o_sc_button").click(function(){
 
3036
            self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val()));
 
3037
            self.destroy();
 
3038
        });
 
3039
        this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){
 
3040
            self.destroy();
 
3041
        });
 
3042
    },
 
3043
});
 
3044
 
 
3045
instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
 
3046
    template: "FieldMany2One",
 
3047
    events: {
 
3048
        'keydown input': function (e) {
 
3049
            switch (e.which) {
 
3050
            case $.ui.keyCode.UP:
 
3051
            case $.ui.keyCode.DOWN:
 
3052
                e.stopPropagation();
 
3053
            }
 
3054
        },
 
3055
    },
 
3056
    init: function(field_manager, node) {
 
3057
        this._super(field_manager, node);
 
3058
        instance.web.form.CompletionFieldMixin.init.call(this);
 
3059
        this.set({'value': false});
 
3060
        this.display_value = {};
 
3061
        this.last_search = [];
 
3062
        this.floating = false;
 
3063
        this.current_display = null;
 
3064
        this.is_started = false;
 
3065
    },
 
3066
    reinit_value: function(val) {
 
3067
        this.internal_set_value(val);
 
3068
        this.floating = false;
 
3069
        if (this.is_started)
 
3070
            this.render_value();
 
3071
    },
 
3072
    initialize_field: function() {
 
3073
        this.is_started = true;
 
3074
        instance.web.bus.on('click', this, function() {
 
3075
            if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) {
 
3076
                this.$input.autocomplete("close");
 
3077
            }
 
3078
        });
 
3079
        instance.web.form.ReinitializeFieldMixin.initialize_field.call(this);
 
3080
    },
 
3081
    initialize_content: function() {
 
3082
        if (!this.get("effective_readonly"))
 
3083
            this.render_editable();
 
3084
    },
 
3085
    destroy_content: function () {
 
3086
        if (this.$drop_down) {
 
3087
            this.$drop_down.off('click');
 
3088
            delete this.$drop_down;
 
3089
        }
 
3090
        if (this.$input) {
 
3091
            this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll');
 
3092
            this.$input.off('keyup blur autocompleteclose autocompleteopen ' +
 
3093
                            'focus focusout change keydown');
 
3094
            delete this.$input;
 
3095
        }
 
3096
        if (this.$follow_button) {
 
3097
            this.$follow_button.off('blur focus click');
 
3098
            delete this.$follow_button;
 
3099
        }
 
3100
    },
 
3101
    destroy: function () {
 
3102
        this.destroy_content();
 
3103
        return this._super();
 
3104
    },
 
3105
    init_error_displayer: function() {
 
3106
        // nothing
 
3107
    },
 
3108
    hide_error_displayer: function() {
 
3109
        // doesn't work
 
3110
    },
 
3111
    show_error_displayer: function() {
 
3112
        new instance.web.form.M2ODialog(this).open();
 
3113
    },
 
3114
    render_editable: function() {
 
3115
        var self = this;
 
3116
        this.$input = this.$el.find("input");
 
3117
 
 
3118
        this.init_error_displayer();
 
3119
 
 
3120
        self.$input.on('focus', function() {
 
3121
            self.hide_error_displayer();
 
3122
        });
 
3123
 
 
3124
        this.$drop_down = this.$el.find(".oe_m2o_drop_down_button");
 
3125
        this.$follow_button = $(".oe_m2o_cm_button", this.$el);
 
3126
 
 
3127
        this.$follow_button.click(function(ev) {
 
3128
            ev.preventDefault();
 
3129
            if (!self.get('value')) {
 
3130
                self.focus();
 
3131
                return;
 
3132
            }
 
3133
            var pop = new instance.web.form.FormOpenPopup(self);
 
3134
            pop.show_element(
 
3135
                self.field.relation,
 
3136
                self.get("value"),
 
3137
                self.build_context(),
 
3138
                {
 
3139
                    title: _t("Open: ") + self.string
 
3140
                }
 
3141
            );
 
3142
            pop.on('write_completed', self, function(){
 
3143
                self.display_value = {};
 
3144
                self.render_value();
 
3145
                self.focus();
 
3146
                self.view.do_onchange(self);
 
3147
            });
 
3148
        });
 
3149
 
 
3150
        // some behavior for input
 
3151
        var input_changed = function() {
 
3152
            if (self.current_display !== self.$input.val()) {
 
3153
                self.current_display = self.$input.val();
 
3154
                if (self.$input.val() === "") {
 
3155
                    self.internal_set_value(false);
 
3156
                    self.floating = false;
 
3157
                } else {
 
3158
                    self.floating = true;
 
3159
                }
 
3160
            }
 
3161
        };
 
3162
        this.$input.keydown(input_changed);
 
3163
        this.$input.change(input_changed);
 
3164
        this.$drop_down.click(function() {
 
3165
            if (self.$input.autocomplete("widget").is(":visible")) {
 
3166
                self.$input.autocomplete("close");
 
3167
                self.$input.focus();
 
3168
            } else {
 
3169
                if (self.get("value") && ! self.floating) {
 
3170
                    self.$input.autocomplete("search", "");
 
3171
                } else {
 
3172
                    self.$input.autocomplete("search");
 
3173
                }
 
3174
            }
 
3175
        });
 
3176
 
 
3177
        // Autocomplete close on dialog content scroll
 
3178
        var close_autocomplete = _.debounce(function() {
 
3179
            if (self.$input.autocomplete("widget").is(":visible")) {
 
3180
                self.$input.autocomplete("close");
 
3181
            }
 
3182
        }, 50);
 
3183
        this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete);
 
3184
 
 
3185
        self.ed_def = $.Deferred();
 
3186
        self.uned_def = $.Deferred();
 
3187
        var ed_delay = 200;
 
3188
        var ed_duration = 15000;
 
3189
        var anyoneLoosesFocus = function (e) {
 
3190
            var used = false;
 
3191
            if (self.floating) {
 
3192
                if (self.last_search.length > 0) {
 
3193
                    if (self.last_search[0][0] != self.get("value")) {
 
3194
                        self.display_value = {};
 
3195
                        self.display_value["" + self.last_search[0][0]] = self.last_search[0][1];
 
3196
                        self.reinit_value(self.last_search[0][0]);
 
3197
                    } else {
 
3198
                        used = true;
 
3199
                        self.render_value();
 
3200
                    }
 
3201
                } else {
 
3202
                    used = true;
 
3203
                    self.reinit_value(false);
 
3204
                }
 
3205
                self.floating = false;
 
3206
            }
 
3207
            if (used && self.get("value") === false && ! self.no_ed) {
 
3208
                self.ed_def.reject();
 
3209
                self.uned_def.reject();
 
3210
                self.ed_def = $.Deferred();
 
3211
                self.ed_def.done(function() {
 
3212
                    self.show_error_displayer();
 
3213
                    ignore_blur = false;
 
3214
                    self.trigger('focused');
 
3215
                });
 
3216
                ignore_blur = true;
 
3217
                setTimeout(function() {
 
3218
                    self.ed_def.resolve();
 
3219
                    self.uned_def.reject();
 
3220
                    self.uned_def = $.Deferred();
 
3221
                    self.uned_def.done(function() {
 
3222
                        self.hide_error_displayer();
 
3223
                    });
 
3224
                    setTimeout(function() {self.uned_def.resolve();}, ed_duration);
 
3225
                }, ed_delay);
 
3226
            } else {
 
3227
                self.no_ed = false;
 
3228
                self.ed_def.reject();
 
3229
            }
 
3230
        };
 
3231
        var ignore_blur = false;
 
3232
        this.$input.on({
 
3233
            focusout: anyoneLoosesFocus,
 
3234
            focus: function () { self.trigger('focused'); },
 
3235
            autocompleteopen: function () { ignore_blur = true; },
 
3236
            autocompleteclose: function () { ignore_blur = false; },
 
3237
            blur: function () {
 
3238
                // autocomplete open
 
3239
                if (ignore_blur) { return; }
 
3240
                if (_(self.getChildren()).any(function (child) {
 
3241
                    return child instanceof instance.web.form.AbstractFormPopup;
 
3242
                })) { return; }
 
3243
                self.trigger('blurred');
 
3244
            }
 
3245
        });
 
3246
 
 
3247
        var isSelecting = false;
 
3248
        // autocomplete
 
3249
        this.$input.autocomplete({
 
3250
            source: function(req, resp) {
 
3251
                self.get_search_result(req.term).done(function(result) {
 
3252
                    resp(result);
 
3253
                });
 
3254
            },
 
3255
            select: function(event, ui) {
 
3256
                isSelecting = true;
 
3257
                var item = ui.item;
 
3258
                if (item.id) {
 
3259
                    self.display_value = {};
 
3260
                    self.display_value["" + item.id] = item.name;
 
3261
                    self.reinit_value(item.id);
 
3262
                } else if (item.action) {
 
3263
                    item.action();
 
3264
                    // Cancel widget blurring, to avoid form blur event
 
3265
                    self.trigger('focused');
 
3266
                    return false;
 
3267
                }
 
3268
            },
 
3269
            focus: function(e, ui) {
 
3270
                e.preventDefault();
 
3271
            },
 
3272
            html: true,
 
3273
            // disabled to solve a bug, but may cause others
 
3274
            //close: anyoneLoosesFocus,
 
3275
            minLength: 0,
 
3276
            delay: 0
 
3277
        });
 
3278
        this.$input.autocomplete("widget").openerpClass();
 
3279
        // used to correct a bug when selecting an element by pushing 'enter' in an editable list
 
3280
        this.$input.keyup(function(e) {
 
3281
            if (e.which === 13) { // ENTER
 
3282
                if (isSelecting)
 
3283
                    e.stopPropagation();
 
3284
            }
 
3285
            isSelecting = false;
 
3286
        });
 
3287
        this.setupFocus(this.$follow_button);
 
3288
    },
 
3289
    render_value: function(no_recurse) {
 
3290
        var self = this;
 
3291
        if (! this.get("value")) {
 
3292
            this.display_string("");
 
3293
            return;
 
3294
        }
 
3295
        var display = this.display_value["" + this.get("value")];
 
3296
        if (display) {
 
3297
            this.display_string(display);
 
3298
            return;
 
3299
        }
 
3300
        if (! no_recurse) {
 
3301
            var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
 
3302
            this.alive(dataset.name_get([self.get("value")])).done(function(data) {
 
3303
                self.display_value["" + self.get("value")] = data[0][1];
 
3304
                self.render_value(true);
 
3305
            });
 
3306
        }
 
3307
    },
 
3308
    display_string: function(str) {
 
3309
        var self = this;
 
3310
        if (!this.get("effective_readonly")) {
 
3311
            this.$input.val(str.split("\n")[0]);
 
3312
            this.current_display = this.$input.val();
 
3313
            if (this.is_false()) {
 
3314
                this.$('.oe_m2o_cm_button').css({'display':'none'});
 
3315
            } else {
 
3316
                this.$('.oe_m2o_cm_button').css({'display':'inline'});
 
3317
            }
 
3318
        } else {
 
3319
            var lines = _.escape(str).split("\n");
 
3320
            var link = "";
 
3321
            var follow = "";
 
3322
            link = lines[0];
 
3323
            follow = _.rest(lines).join("<br />");
 
3324
            if (follow)
 
3325
                link += "<br />";
 
3326
            var $link = this.$el.find('.oe_form_uri')
 
3327
                 .unbind('click')
 
3328
                 .html(link);
 
3329
            if (! this.options.no_open)
 
3330
                $link.click(function () {
 
3331
                    self.do_action({
 
3332
                        type: 'ir.actions.act_window',
 
3333
                        res_model: self.field.relation,
 
3334
                        res_id: self.get("value"),
 
3335
                        views: [[false, 'form']],
 
3336
                        target: 'current',
 
3337
                        context: self.build_context().eval(),
 
3338
                    });
 
3339
                    return false;
 
3340
                 });
 
3341
            $(".oe_form_m2o_follow", this.$el).html(follow);
 
3342
        }
 
3343
    },
 
3344
    set_value: function(value_) {
 
3345
        var self = this;
 
3346
        if (value_ instanceof Array) {
 
3347
            this.display_value = {};
 
3348
            if (! this.options.always_reload) {
 
3349
                this.display_value["" + value_[0]] = value_[1];
 
3350
            }
 
3351
            value_ = value_[0];
 
3352
        }
 
3353
        value_ = value_ || false;
 
3354
        this.reinit_value(value_);
 
3355
    },
 
3356
    get_displayed: function() {
 
3357
        return this.display_value["" + this.get("value")];
 
3358
    },
 
3359
    add_id: function(id) {
 
3360
        this.display_value = {};
 
3361
        this.reinit_value(id);
 
3362
    },
 
3363
    is_false: function() {
 
3364
        return ! this.get("value");
 
3365
    },
 
3366
    focus: function () {
 
3367
        var input = !this.get('effective_readonly') && this.$input && this.$input[0];
 
3368
        return input ? input.focus() : false;
 
3369
    },
 
3370
    _quick_create: function() {
 
3371
        this.no_ed = true;
 
3372
        this.ed_def.reject();
 
3373
        return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments);
 
3374
    },
 
3375
    _search_create_popup: function() {
 
3376
        this.no_ed = true;
 
3377
        this.ed_def.reject();
 
3378
        return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments);
 
3379
    },
 
3380
    set_dimensions: function (height, width) {
 
3381
        this._super(height, width);
 
3382
        this.$input.css('height', height);
 
3383
    }
 
3384
});
 
3385
 
 
3386
instance.web.form.Many2OneButton = instance.web.form.AbstractField.extend({
 
3387
    template: 'Many2OneButton',
 
3388
    init: function(field_manager, node) {
 
3389
        this._super.apply(this, arguments);
 
3390
    },
 
3391
    start: function() {
 
3392
        this._super.apply(this, arguments);
 
3393
        this.set_button();
 
3394
    },
 
3395
    set_button: function() {
 
3396
        var self = this;
 
3397
        if (this.$button) {
 
3398
            this.$button.remove();
 
3399
        }
 
3400
        this.string = '';
 
3401
        this.node.attrs.icon = this.get('value') ? '/web/static/src/img/icons/gtk-yes.png' : '/web/static/src/img/icons/gtk-no.png';
 
3402
        this.$button = $(QWeb.render('WidgetButton', {'widget': this}));
 
3403
        this.$button.addClass('oe_link').css({'padding':'4px'});
 
3404
        this.$el.append(this.$button);
 
3405
        this.$button.on('click', self.on_click);
 
3406
    },
 
3407
    on_click: function(ev) {
 
3408
        var self = this;
 
3409
        this.popup =  new instance.web.form.FormOpenPopup(this);
 
3410
        this.popup.show_element(
 
3411
            this.field.relation,
 
3412
            this.get('value'),
 
3413
            this.build_context(),
 
3414
            {title: this.string}
 
3415
        );
 
3416
        this.popup.on('create_completed', self, function(r) {
 
3417
            self.set_value(r);
 
3418
        });
 
3419
    },
 
3420
    set_value: function(value_) {
 
3421
        var self = this;
 
3422
        if (value_ instanceof Array) {
 
3423
            value_ = value_[0];
 
3424
        }
 
3425
        value_ = value_ || false;
 
3426
        this.set('value', value_);
 
3427
        this.set_button();
 
3428
     },
 
3429
});
 
3430
 
 
3431
/*
 
3432
# Values: (0, 0,  { fields })    create
 
3433
#         (1, ID, { fields })    update
 
3434
#         (2, ID)                remove (delete)
 
3435
#         (3, ID)                unlink one (target id or target of relation)
 
3436
#         (4, ID)                link
 
3437
#         (5)                    unlink all (only valid for one2many)
 
3438
*/
 
3439
var commands = {
 
3440
    // (0, _, {values})
 
3441
    CREATE: 0,
 
3442
    'create': function (values) {
 
3443
        return [commands.CREATE, false, values];
 
3444
    },
 
3445
    // (1, id, {values})
 
3446
    UPDATE: 1,
 
3447
    'update': function (id, values) {
 
3448
        return [commands.UPDATE, id, values];
 
3449
    },
 
3450
    // (2, id[, _])
 
3451
    DELETE: 2,
 
3452
    'delete': function (id) {
 
3453
        return [commands.DELETE, id, false];
 
3454
    },
 
3455
    // (3, id[, _]) removes relation, but not linked record itself
 
3456
    FORGET: 3,
 
3457
    'forget': function (id) {
 
3458
        return [commands.FORGET, id, false];
 
3459
    },
 
3460
    // (4, id[, _])
 
3461
    LINK_TO: 4,
 
3462
    'link_to': function (id) {
 
3463
        return [commands.LINK_TO, id, false];
 
3464
    },
 
3465
    // (5[, _[, _]])
 
3466
    DELETE_ALL: 5,
 
3467
    'delete_all': function () {
 
3468
        return [5, false, false];
 
3469
    },
 
3470
    // (6, _, ids) replaces all linked records with provided ids
 
3471
    REPLACE_WITH: 6,
 
3472
    'replace_with': function (ids) {
 
3473
        return [6, false, ids];
 
3474
    }
 
3475
};
 
3476
instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({
 
3477
    multi_selection: false,
 
3478
    disable_utility_classes: true,
 
3479
    init: function(field_manager, node) {
 
3480
        this._super(field_manager, node);
 
3481
        lazy_build_o2m_kanban_view();
 
3482
        this.is_loaded = $.Deferred();
 
3483
        this.initial_is_loaded = this.is_loaded;
 
3484
        this.form_last_update = $.Deferred();
 
3485
        this.init_form_last_update = this.form_last_update;
 
3486
        this.is_started = false;
 
3487
        this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation);
 
3488
        this.dataset.o2m = this;
 
3489
        this.dataset.parent_view = this.view;
 
3490
        this.dataset.child_name = this.name;
 
3491
        var self = this;
 
3492
        this.dataset.on('dataset_changed', this, function() {
 
3493
            self.trigger_on_change();
 
3494
        });
 
3495
        this.set_value([]);
 
3496
    },
 
3497
    start: function() {
 
3498
        this._super.apply(this, arguments);
 
3499
        this.$el.addClass('oe_form_field oe_form_field_one2many');
 
3500
 
 
3501
        var self = this;
 
3502
 
 
3503
        self.load_views();
 
3504
        this.is_loaded.done(function() {
 
3505
            self.on("change:effective_readonly", self, function() {
 
3506
                self.is_loaded = self.is_loaded.then(function() {
 
3507
                    self.viewmanager.destroy();
 
3508
                    return $.when(self.load_views()).done(function() {
 
3509
                        self.reload_current_view();
 
3510
                    });
 
3511
                });
 
3512
            });
 
3513
        });
 
3514
        this.is_started = true;
 
3515
        this.reload_current_view();
 
3516
    },
 
3517
    trigger_on_change: function() {
 
3518
        this.trigger('changed_value');
 
3519
    },
 
3520
    load_views: function() {
 
3521
        var self = this;
 
3522
 
 
3523
        var modes = this.node.attrs.mode;
 
3524
        modes = !!modes ? modes.split(",") : ["tree"];
 
3525
        var views = [];
 
3526
        _.each(modes, function(mode) {
 
3527
            if (! _.include(["list", "tree", "graph", "kanban"], mode)) {
 
3528
                throw new Error(_.str.sprintf(_t("View type '%s' is not supported in One2Many."), mode));
 
3529
            }
 
3530
            var view = {
 
3531
                view_id: false,
 
3532
                view_type: mode == "tree" ? "list" : mode,
 
3533
                options: {}
 
3534
            };
 
3535
            if (self.field.views && self.field.views[mode]) {
 
3536
                view.embedded_view = self.field.views[mode];
 
3537
            }
 
3538
            if(view.view_type === "list") {
 
3539
                _.extend(view.options, {
 
3540
                    addable: null,
 
3541
                    selectable: self.multi_selection,
 
3542
                    sortable: false,
 
3543
                    import_enabled: false,
 
3544
                    deletable: true
 
3545
                });
 
3546
                if (self.get("effective_readonly")) {
 
3547
                    _.extend(view.options, {
 
3548
                        deletable: null,
 
3549
                        reorderable: false,
 
3550
                    });
 
3551
                }
 
3552
            } else if (view.view_type === "form") {
 
3553
                if (self.get("effective_readonly")) {
 
3554
                    view.view_type = 'form';
 
3555
                }
 
3556
                _.extend(view.options, {
 
3557
                    not_interactible_on_create: true,
 
3558
                });
 
3559
            } else if (view.view_type === "kanban") {
 
3560
                _.extend(view.options, {
 
3561
                    confirm_on_delete: false,
 
3562
                });
 
3563
                if (self.get("effective_readonly")) {
 
3564
                    _.extend(view.options, {
 
3565
                        action_buttons: false,
 
3566
                        quick_creatable: false,
 
3567
                        creatable: false,
 
3568
                        read_only_mode: true,
 
3569
                    });
 
3570
                }
 
3571
            }
 
3572
            views.push(view);
 
3573
        });
 
3574
        this.views = views;
 
3575
 
 
3576
        this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {});
 
3577
        this.viewmanager.o2m = self;
 
3578
        var once = $.Deferred().done(function() {
 
3579
            self.init_form_last_update.resolve();
 
3580
        });
 
3581
        var def = $.Deferred().done(function() {
 
3582
            self.initial_is_loaded.resolve();
 
3583
        });
 
3584
        this.viewmanager.on("controller_inited", self, function(view_type, controller) {
 
3585
            controller.o2m = self;
 
3586
            if (view_type == "list") {
 
3587
                if (self.get("effective_readonly")) {
 
3588
                    controller.on('edit:before', self, function (e) {
 
3589
                        e.cancel = true;
 
3590
                    });
 
3591
                    _(controller.columns).find(function (column) {
 
3592
                        if (!(column instanceof instance.web.list.Handle)) {
 
3593
                            return false;
 
3594
                        }
 
3595
                        column.modifiers.invisible = true;
 
3596
                        return true;
 
3597
                    });
 
3598
                }
 
3599
            } else if (view_type === "form") {
 
3600
                if (self.get("effective_readonly")) {
 
3601
                    $(".oe_form_buttons", controller.$el).children().remove();
 
3602
                }
 
3603
                controller.on("load_record", self, function(){
 
3604
                     once.resolve();
 
3605
                 });
 
3606
                controller.on('pager_action_executed',self,self.save_any_view);
 
3607
            } else if (view_type == "graph") {
 
3608
                self.reload_current_view()
 
3609
            }
 
3610
            def.resolve();
 
3611
        });
 
3612
        this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) {
 
3613
            $.when(self.save_any_view()).done(function() {
 
3614
                if (n_mode === "list") {
 
3615
                    $.async_when().done(function() {
 
3616
                        self.reload_current_view();
 
3617
                    });
 
3618
                }
 
3619
            });
 
3620
        });
 
3621
        $.async_when().done(function () {
 
3622
            self.viewmanager.appendTo(self.$el);
 
3623
        });
 
3624
        return def;
 
3625
    },
 
3626
    reload_current_view: function() {
 
3627
        var self = this;
 
3628
        return self.is_loaded = self.is_loaded.then(function() {
 
3629
            var active_view = self.viewmanager.active_view;
 
3630
            var view = self.viewmanager.views[active_view].controller;
 
3631
            if(active_view === "list") {
 
3632
                return view.reload_content();
 
3633
            } else if (active_view === "form") {
 
3634
                if (self.dataset.index === null && self.dataset.ids.length >= 1) {
 
3635
                    self.dataset.index = 0;
 
3636
                }
 
3637
                var act = function() {
 
3638
                    return view.do_show();
 
3639
                };
 
3640
                self.form_last_update = self.form_last_update.then(act, act);
 
3641
                return self.form_last_update;
 
3642
            } else if (view.do_search) {
 
3643
                return view.do_search(self.build_domain(), self.dataset.get_context(), []);
 
3644
            }
 
3645
        }, undefined);
 
3646
    },
 
3647
    set_value: function(value_) {
 
3648
        value_ = value_ || [];
 
3649
        var self = this;
 
3650
        this.dataset.reset_ids([]);
 
3651
        if(value_.length >= 1 && value_[0] instanceof Array) {
 
3652
            var ids = [];
 
3653
            _.each(value_, function(command) {
 
3654
                var obj = {values: command[2]};
 
3655
                switch (command[0]) {
 
3656
                    case commands.CREATE:
 
3657
                        obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
 
3658
                        obj.defaults = {};
 
3659
                        self.dataset.to_create.push(obj);
 
3660
                        self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
 
3661
                        ids.push(obj.id);
 
3662
                        return;
 
3663
                    case commands.UPDATE:
 
3664
                        obj['id'] = command[1];
 
3665
                        self.dataset.to_write.push(obj);
 
3666
                        self.dataset.cache.push(_.extend(_.clone(obj), {values: _.clone(command[2])}));
 
3667
                        ids.push(obj.id);
 
3668
                        return;
 
3669
                    case commands.DELETE:
 
3670
                        self.dataset.to_delete.push({id: command[1]});
 
3671
                        return;
 
3672
                    case commands.LINK_TO:
 
3673
                        ids.push(command[1]);
 
3674
                        return;
 
3675
                    case commands.DELETE_ALL:
 
3676
                        self.dataset.delete_all = true;
 
3677
                        return;
 
3678
                }
 
3679
            });
 
3680
            this._super(ids);
 
3681
            this.dataset.set_ids(ids);
 
3682
        } else if (value_.length >= 1 && typeof(value_[0]) === "object") {
 
3683
            var ids = [];
 
3684
            this.dataset.delete_all = true;
 
3685
            _.each(value_, function(command) {
 
3686
                var obj = {values: command};
 
3687
                obj['id'] = _.uniqueId(self.dataset.virtual_id_prefix);
 
3688
                obj.defaults = {};
 
3689
                self.dataset.to_create.push(obj);
 
3690
                self.dataset.cache.push(_.clone(obj));
 
3691
                ids.push(obj.id);
 
3692
            });
 
3693
            this._super(ids);
 
3694
            this.dataset.set_ids(ids);
 
3695
        } else {
 
3696
            this._super(value_);
 
3697
            this.dataset.reset_ids(value_);
 
3698
        }
 
3699
        if (this.dataset.index === null && this.dataset.ids.length > 0) {
 
3700
            this.dataset.index = 0;
 
3701
        }
 
3702
        this.trigger_on_change();
 
3703
        if (this.is_started) {
 
3704
            return self.reload_current_view();
 
3705
        } else {
 
3706
            return $.when();
 
3707
        }
 
3708
    },
 
3709
    get_value: function() {
 
3710
        var self = this;
 
3711
        if (!this.dataset)
 
3712
            return [];
 
3713
        var val = this.dataset.delete_all ? [commands.delete_all()] : [];
 
3714
        val = val.concat(_.map(this.dataset.ids, function(id) {
 
3715
            var alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;});
 
3716
            if (alter_order) {
 
3717
                return commands.create(alter_order.values);
 
3718
            }
 
3719
            alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;});
 
3720
            if (alter_order) {
 
3721
                return commands.update(alter_order.id, alter_order.values);
 
3722
            }
 
3723
            return commands.link_to(id);
 
3724
        }));
 
3725
        return val.concat(_.map(
 
3726
            this.dataset.to_delete, function(x) {
 
3727
                return commands['delete'](x.id);}));
 
3728
    },
 
3729
    commit_value: function() {
 
3730
        return this.save_any_view();
 
3731
    },
 
3732
    save_any_view: function() {
 
3733
        if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view &&
 
3734
            this.viewmanager.views[this.viewmanager.active_view] &&
 
3735
            this.viewmanager.views[this.viewmanager.active_view].controller) {
 
3736
            var view = this.viewmanager.views[this.viewmanager.active_view].controller;
 
3737
            if (this.viewmanager.active_view === "form") {
 
3738
                if (!view.is_initialized.state() === 'resolved') {
 
3739
                    return $.when(false);
 
3740
                }
 
3741
                return $.when(view.save());
 
3742
            } else if (this.viewmanager.active_view === "list") {
 
3743
                return $.when(view.ensure_saved());
 
3744
            }
 
3745
        }
 
3746
        return $.when(false);
 
3747
    },
 
3748
    is_syntax_valid: function() {
 
3749
        if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view])
 
3750
            return true;
 
3751
        var view = this.viewmanager.views[this.viewmanager.active_view].controller;
 
3752
        switch (this.viewmanager.active_view) {
 
3753
        case 'form':
 
3754
            return _(view.fields).chain()
 
3755
                .invoke('is_valid')
 
3756
                .all(_.identity)
 
3757
                .value();
 
3758
            break;
 
3759
        case 'list':
 
3760
            return view.is_valid();
 
3761
        }
 
3762
        return true;
 
3763
    },
 
3764
});
 
3765
 
 
3766
instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({
 
3767
    template: 'One2Many.viewmanager',
 
3768
    init: function(parent, dataset, views, flags) {
 
3769
        this._super(parent, dataset, views, _.extend({}, flags, {$sidebar: false}));
 
3770
        this.registry = this.registry.extend({
 
3771
            list: 'instance.web.form.One2ManyListView',
 
3772
            form: 'instance.web.form.One2ManyFormView',
 
3773
            kanban: 'instance.web.form.One2ManyKanbanView',
 
3774
        });
 
3775
        this.__ignore_blur = false;
 
3776
    },
 
3777
    switch_mode: function(mode, unused) {
 
3778
        if (mode !== 'form') {
 
3779
            return this._super(mode, unused);
 
3780
        }
 
3781
        var self = this;
 
3782
        var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null;
 
3783
        var pop = new instance.web.form.FormOpenPopup(this);
 
3784
        pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
 
3785
            title: _t("Open: ") + self.o2m.string,
 
3786
            create_function: function(data, options) {
 
3787
                return self.o2m.dataset.create(data, options).done(function(r) {
 
3788
                    self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
 
3789
                    self.o2m.dataset.trigger("dataset_changed", r);
 
3790
                });
 
3791
            },
 
3792
            write_function: function(id, data, options) {
 
3793
                return self.o2m.dataset.write(id, data, {}).done(function() {
 
3794
                    self.o2m.reload_current_view();
 
3795
                });
 
3796
            },
 
3797
            alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
 
3798
            parent_view: self.o2m.view,
 
3799
            child_name: self.o2m.name,
 
3800
            read_function: function() {
 
3801
                return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
 
3802
            },
 
3803
            form_view_options: {'not_interactible_on_create':true},
 
3804
            readonly: self.o2m.get("effective_readonly")
 
3805
        });
 
3806
        pop.on("elements_selected", self, function() {
 
3807
            self.o2m.reload_current_view();
 
3808
        });
 
3809
    },
 
3810
});
 
3811
 
 
3812
instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({
 
3813
    get_context: function() {
 
3814
        this.context = this.o2m.build_context();
 
3815
        return this.context;
 
3816
    }
 
3817
});
 
3818
 
 
3819
instance.web.form.One2ManyListView = instance.web.ListView.extend({
 
3820
    _template: 'One2Many.listview',
 
3821
    init: function (parent, dataset, view_id, options) {
 
3822
        this._super(parent, dataset, view_id, _.extend(options || {}, {
 
3823
            GroupsType: instance.web.form.One2ManyGroups,
 
3824
            ListType: instance.web.form.One2ManyList
 
3825
        }));
 
3826
        this.on('edit:before', this, this.proxy('_before_edit'));
 
3827
        this.on('edit:after', this, this.proxy('_after_edit'));
 
3828
        this.on('save:before cancel:before', this, this.proxy('_before_unedit'));
 
3829
 
 
3830
        this.records
 
3831
            .bind('add', this.proxy("changed_records"))
 
3832
            .bind('edit', this.proxy("changed_records"))
 
3833
            .bind('remove', this.proxy("changed_records"));
 
3834
    },
 
3835
    start: function () {
 
3836
        var ret = this._super();
 
3837
        this.$el
 
3838
            .off('mousedown.handleButtons')
 
3839
            .on('mousedown.handleButtons', 'table button', this.proxy('_button_down'));
 
3840
        return ret;
 
3841
    },
 
3842
    changed_records: function () {
 
3843
        this.o2m.trigger_on_change();
 
3844
    },
 
3845
    is_valid: function () {
 
3846
        var editor = this.editor;
 
3847
        var form = editor.form;
 
3848
        // If no edition is pending, the listview can not be invalid (?)
 
3849
        if (!editor.record) {
 
3850
            return true
 
3851
        }
 
3852
        // If the form has not been modified, the view can only be valid
 
3853
        // NB: is_dirty will also be set on defaults/onchanges/whatever?
 
3854
        // oe_form_dirty seems to only be set on actual user actions
 
3855
        if (!form.$el.is('.oe_form_dirty')) {
 
3856
            return true;
 
3857
        }
 
3858
        this.o2m._dirty_flag = true;
 
3859
 
 
3860
        // Otherwise validate internal form
 
3861
        return _(form.fields).chain()
 
3862
            .invoke(function () {
 
3863
                this._check_css_flags();
 
3864
                return this.is_valid();
 
3865
            })
 
3866
            .all(_.identity)
 
3867
            .value();
 
3868
    },
 
3869
    do_add_record: function () {
 
3870
        if (this.editable()) {
 
3871
            this._super.apply(this, arguments);
 
3872
        } else {
 
3873
            var self = this;
 
3874
            var pop = new instance.web.form.SelectCreatePopup(this);
 
3875
            pop.select_element(
 
3876
                self.o2m.field.relation,
 
3877
                {
 
3878
                    title: _t("Create: ") + self.o2m.string,
 
3879
                    initial_view: "form",
 
3880
                    alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
 
3881
                    create_function: function(data, options) {
 
3882
                        return self.o2m.dataset.create(data, options).done(function(r) {
 
3883
                            self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r]));
 
3884
                            self.o2m.dataset.trigger("dataset_changed", r);
 
3885
                        });
 
3886
                    },
 
3887
                    read_function: function() {
 
3888
                        return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
 
3889
                    },
 
3890
                    parent_view: self.o2m.view,
 
3891
                    child_name: self.o2m.name,
 
3892
                    form_view_options: {'not_interactible_on_create':true}
 
3893
                },
 
3894
                self.o2m.build_domain(),
 
3895
                self.o2m.build_context()
 
3896
            );
 
3897
            pop.on("elements_selected", self, function() {
 
3898
                self.o2m.reload_current_view();
 
3899
            });
 
3900
        }
 
3901
    },
 
3902
    do_activate_record: function(index, id) {
 
3903
        var self = this;
 
3904
        var pop = new instance.web.form.FormOpenPopup(self);
 
3905
        pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), {
 
3906
            title: _t("Open: ") + self.o2m.string,
 
3907
            write_function: function(id, data) {
 
3908
                return self.o2m.dataset.write(id, data, {}).done(function() {
 
3909
                    self.o2m.reload_current_view();
 
3910
                });
 
3911
            },
 
3912
            alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined,
 
3913
            parent_view: self.o2m.view,
 
3914
            child_name: self.o2m.name,
 
3915
            read_function: function() {
 
3916
                return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments);
 
3917
            },
 
3918
            form_view_options: {'not_interactible_on_create':true},
 
3919
            readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly")
 
3920
        });
 
3921
    },
 
3922
    do_button_action: function (name, id, callback) {
 
3923
        if (!_.isNumber(id)) {
 
3924
            instance.webclient.notification.warn(
 
3925
                _t("Action Button"),
 
3926
                _t("The o2m record must be saved before an action can be used"));
 
3927
            return;
 
3928
        }
 
3929
        var parent_form = this.o2m.view;
 
3930
        var self = this;
 
3931
        this.ensure_saved().then(function () {
 
3932
            if (parent_form)
 
3933
                return parent_form.save();
 
3934
            else
 
3935
                return $.when();
 
3936
        }).done(function () {
 
3937
            self.handle_button(name, id, callback);
 
3938
        });
 
3939
    },
 
3940
 
 
3941
    _before_edit: function () {
 
3942
        this.__ignore_blur = false;
 
3943
        this.editor.form.on('blurred', this, this._on_form_blur);
 
3944
    },
 
3945
    _after_edit: function () {
 
3946
        // The form's blur thing may be jiggered during the edition setup,
 
3947
        // potentially leading to the o2m instasaving the row. Cancel any
 
3948
        // blurring triggered the edition startup here
 
3949
        this.editor.form.widgetFocused();
 
3950
    },
 
3951
    _before_unedit: function () {
 
3952
        this.editor.form.off('blurred', this, this._on_form_blur);
 
3953
    },
 
3954
    _button_down: function () {
 
3955
        // If a button is clicked (usually some sort of action button), it's
 
3956
        // the button's responsibility to ensure the editable list is in the
 
3957
        // correct state -> ignore form blurring
 
3958
        this.__ignore_blur = true;
 
3959
    },
 
3960
    /**
 
3961
     * Handles blurring of the nested form (saves the currently edited row),
 
3962
     * unless the flag to ignore the event is set to ``true``
 
3963
     *
 
3964
     * Makes the internal form go away
 
3965
     */
 
3966
    _on_form_blur: function () {
 
3967
        if (this.__ignore_blur) {
 
3968
            this.__ignore_blur = false;
 
3969
            return;
 
3970
        }
 
3971
        // FIXME: why isn't there an API for this?
 
3972
        if (this.editor.form.$el.hasClass('oe_form_dirty')) {
 
3973
            this.ensure_saved();
 
3974
            return;
 
3975
        }
 
3976
        this.cancel_edition();
 
3977
    },
 
3978
    keyup_ENTER: function () {
 
3979
        // blurring caused by hitting the [Return] key, should skip the
 
3980
        // autosave-on-blur and let the handler for [Return] do its thing (save
 
3981
        // the current row *anyway*, then create a new one/edit the next one)
 
3982
        this.__ignore_blur = true;
 
3983
        this._super.apply(this, arguments);
 
3984
    },
 
3985
    do_delete: function (ids) {
 
3986
        var confirm = window.confirm;
 
3987
        window.confirm = function () { return true; };
 
3988
        try {
 
3989
            return this._super(ids);
 
3990
        } finally {
 
3991
            window.confirm = confirm;
 
3992
        }
 
3993
    }
 
3994
});
 
3995
instance.web.form.One2ManyGroups = instance.web.ListView.Groups.extend({
 
3996
    setup_resequence_rows: function () {
 
3997
        if (!this.view.o2m.get('effective_readonly')) {
 
3998
            this._super.apply(this, arguments);
 
3999
        }
 
4000
    }
 
4001
});
 
4002
instance.web.form.One2ManyList = instance.web.ListView.List.extend({
 
4003
    pad_table_to: function (count) {
 
4004
        if (!this.view.is_action_enabled('create')) {
 
4005
            this._super(count);
 
4006
        } else {
 
4007
            this._super(count > 0 ? count - 1 : 0);
 
4008
        }
 
4009
 
 
4010
        // magical invocation of wtf does that do
 
4011
        if (this.view.o2m.get('effective_readonly')) {
 
4012
            return;
 
4013
        }
 
4014
 
 
4015
        var self = this;
 
4016
        var columns = _(this.columns).filter(function (column) {
 
4017
            return column.invisible !== '1';
 
4018
        }).length;
 
4019
        if (this.options.selectable) { columns++; }
 
4020
        if (this.options.deletable) { columns++; }
 
4021
 
 
4022
        if (!this.view.is_action_enabled('create')) {
 
4023
            return;
 
4024
        }
 
4025
 
 
4026
        var $cell = $('<td>', {
 
4027
            colspan: columns,
 
4028
            'class': 'oe_form_field_one2many_list_row_add'
 
4029
        }).append(
 
4030
            $('<a>', {href: '#'}).text(_t("Add an item"))
 
4031
                .mousedown(function () {
 
4032
                    // FIXME: needs to be an official API somehow
 
4033
                    if (self.view.editor.is_editing()) {
 
4034
                        self.view.__ignore_blur = true;
 
4035
                    }
 
4036
                })
 
4037
                .click(function (e) {
 
4038
                    e.preventDefault();
 
4039
                    e.stopPropagation();
 
4040
                    // FIXME: there should also be an API for that one
 
4041
                    if (self.view.editor.form.__blur_timeout) {
 
4042
                        clearTimeout(self.view.editor.form.__blur_timeout);
 
4043
                        self.view.editor.form.__blur_timeout = false;
 
4044
                    }
 
4045
                    self.view.ensure_saved().done(function () {
 
4046
                        self.view.do_add_record();
 
4047
                    });
 
4048
                }));
 
4049
 
 
4050
        var $padding = this.$current.find('tr:not([data-id]):first');
 
4051
        var $newrow = $('<tr>').append($cell);
 
4052
        if ($padding.length) {
 
4053
            $padding.before($newrow);
 
4054
        } else {
 
4055
            this.$current.append($newrow)
 
4056
        }
 
4057
    }
 
4058
});
 
4059
 
 
4060
instance.web.form.One2ManyFormView = instance.web.FormView.extend({
 
4061
    form_template: 'One2Many.formview',
 
4062
    load_form: function(data) {
 
4063
        this._super(data);
 
4064
        var self = this;
 
4065
        this.$buttons.find('button.oe_form_button_create').click(function() {
 
4066
            self.save().done(self.on_button_new);
 
4067
        });
 
4068
    },
 
4069
    do_notify_change: function() {
 
4070
        if (this.dataset.parent_view) {
 
4071
            this.dataset.parent_view.do_notify_change();
 
4072
        } else {
 
4073
            this._super.apply(this, arguments);
 
4074
        }
 
4075
    }
 
4076
});
 
4077
 
 
4078
var lazy_build_o2m_kanban_view = function() {
 
4079
    if (! instance.web_kanban || instance.web.form.One2ManyKanbanView)
 
4080
        return;
 
4081
    instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({
 
4082
    });
 
4083
};
 
4084
 
 
4085
instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
 
4086
    template: "FieldMany2ManyTags",
 
4087
    init: function() {
 
4088
        this._super.apply(this, arguments);
 
4089
        instance.web.form.CompletionFieldMixin.init.call(this);
 
4090
        this.set({"value": []});
 
4091
        this._display_orderer = new instance.web.DropMisordered();
 
4092
        this._drop_shown = false;
 
4093
    },
 
4094
    initialize_content: function() {
 
4095
        if (this.get("effective_readonly"))
 
4096
            return;
 
4097
        var self = this;
 
4098
        var ignore_blur = false;
 
4099
        self.$text = this.$("textarea");
 
4100
        self.$text.textext({
 
4101
            plugins : 'tags arrow autocomplete',
 
4102
            autocomplete: {
 
4103
                render: function(suggestion) {
 
4104
                    return $('<span class="text-label"/>').
 
4105
                             data('index', suggestion['index']).html(suggestion['label']);
 
4106
                }
 
4107
            },
 
4108
            ext: {
 
4109
                autocomplete: {
 
4110
                    selectFromDropdown: function() {
 
4111
                        this.trigger('hideDropdown');
 
4112
                        var index = Number(this.selectedSuggestionElement().children().children().data('index'));
 
4113
                        var data = self.search_result[index];
 
4114
                        if (data.id) {
 
4115
                            self.add_id(data.id);
 
4116
                        } else {
 
4117
                            ignore_blur = true;
 
4118
                            data.action();
 
4119
                        }
 
4120
                        this.trigger('setSuggestions', {result : []});
 
4121
                    },
 
4122
                },
 
4123
                tags: {
 
4124
                    isTagAllowed: function(tag) {
 
4125
                        return !!tag.name;
 
4126
 
 
4127
                    },
 
4128
                    removeTag: function(tag) {
 
4129
                        var id = tag.data("id");
 
4130
                        self.set({"value": _.without(self.get("value"), id)});
 
4131
                    },
 
4132
                    renderTag: function(stuff) {
 
4133
                        return $.fn.textext.TextExtTags.prototype.renderTag.
 
4134
                            call(this, stuff).data("id", stuff.id);
 
4135
                    },
 
4136
                },
 
4137
                itemManager: {
 
4138
                    itemToString: function(item) {
 
4139
                        return item.name;
 
4140
                    },
 
4141
                },
 
4142
                core: {
 
4143
                    onSetInputData: function(e, data) {
 
4144
                        if (data == '') {
 
4145
                            this._plugins.autocomplete._suggestions = null;
 
4146
                        }
 
4147
                        this.input().val(data);
 
4148
                    },
 
4149
                },
 
4150
            },
 
4151
        }).bind('getSuggestions', function(e, data) {
 
4152
            var _this = this;
 
4153
            var str = !!data ? data.query || '' : '';
 
4154
            self.get_search_result(str).done(function(result) {
 
4155
                self.search_result = result;
 
4156
                $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
 
4157
                    return _.extend(el, {index:i});
 
4158
                })});
 
4159
            });
 
4160
        }).bind('hideDropdown', function() {
 
4161
            self._drop_shown = false;
 
4162
        }).bind('showDropdown', function() {
 
4163
            self._drop_shown = true;
 
4164
        });
 
4165
        self.tags = self.$text.textext()[0].tags();
 
4166
        self.$text
 
4167
            .focusin(function () {
 
4168
                self.trigger('focused');
 
4169
                ignore_blur = false;
 
4170
            })
 
4171
            .focusout(function() {
 
4172
                self.$text.trigger("setInputData", "");
 
4173
                if (!ignore_blur) {
 
4174
                    self.trigger('blurred');
 
4175
                }
 
4176
            }).keydown(function(e) {
 
4177
                if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
 
4178
                    self.$text.textext()[0].autocomplete().selectFromDropdown();
 
4179
                }
 
4180
            });
 
4181
    },
 
4182
    set_value: function(value_) {
 
4183
        value_ = value_ || [];
 
4184
        if (value_.length >= 1 && value_[0] instanceof Array) {
 
4185
            value_ = value_[0][2];
 
4186
        }
 
4187
        this._super(value_);
 
4188
    },
 
4189
    is_false: function() {
 
4190
        return _(this.get("value")).isEmpty();
 
4191
    },
 
4192
    get_value: function() {
 
4193
        var tmp = [commands.replace_with(this.get("value"))];
 
4194
        return tmp;
 
4195
    },
 
4196
    get_search_blacklist: function() {
 
4197
        return this.get("value");
 
4198
    },
 
4199
    render_value: function() {
 
4200
        var self = this;
 
4201
        var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context());
 
4202
        var values = self.get("value");
 
4203
        var handle_names = function(data) {
 
4204
            if (self.isDestroyed())
 
4205
                return;
 
4206
            var indexed = {};
 
4207
            _.each(data, function(el) {
 
4208
                indexed[el[0]] = el;
 
4209
            });
 
4210
            data = _.map(values, function(el) { return indexed[el]; });
 
4211
            if (! self.get("effective_readonly")) {
 
4212
                self.tags.containerElement().children().remove();
 
4213
                self.$('textarea').css("padding-left", "3px");
 
4214
                self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};}));
 
4215
            } else {
 
4216
                self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data}));
 
4217
            }
 
4218
        };
 
4219
        if (! values || values.length > 0) {
 
4220
            this._display_orderer.add(dataset.name_get(values)).done(handle_names);
 
4221
        } else {
 
4222
            handle_names([]);
 
4223
        }
 
4224
    },
 
4225
    add_id: function(id) {
 
4226
        this.set({'value': _.uniq(this.get('value').concat([id]))});
 
4227
    },
 
4228
    focus: function () {
 
4229
        var input = this.$text && this.$text[0];
 
4230
        return input ? input.focus() : false;
 
4231
    },
 
4232
});
 
4233
 
 
4234
/**
 
4235
    widget options:
 
4236
    - reload_on_button: Reload the whole form view if click on a button in a list view.
 
4237
        If you see this options, do not use it, it's basically a dirty hack to make one
 
4238
        precise o2m to behave the way we want.
 
4239
*/
 
4240
instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 
4241
    multi_selection: false,
 
4242
    disable_utility_classes: true,
 
4243
    init: function(field_manager, node) {
 
4244
        this._super(field_manager, node);
 
4245
        this.is_loaded = $.Deferred();
 
4246
        this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
 
4247
        this.dataset.m2m = this;
 
4248
        var self = this;
 
4249
        this.dataset.on('unlink', self, function(ids) {
 
4250
            self.dataset_changed();
 
4251
        });
 
4252
        this.set_value([]);
 
4253
        this.list_dm = new instance.web.DropMisordered();
 
4254
        this.render_value_dm = new instance.web.DropMisordered();
 
4255
    },
 
4256
    initialize_content: function() {
 
4257
        var self = this;
 
4258
 
 
4259
        this.$el.addClass('oe_form_field oe_form_field_many2many');
 
4260
 
 
4261
        this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, {
 
4262
                    'addable': this.get("effective_readonly") ? null : _t("Add"),
 
4263
                    'deletable': this.get("effective_readonly") ? false : true,
 
4264
                    'selectable': this.multi_selection,
 
4265
                    'sortable': false,
 
4266
                    'reorderable': false,
 
4267
                    'import_enabled': false,
 
4268
            });
 
4269
        var embedded = (this.field.views || {}).tree;
 
4270
        if (embedded) {
 
4271
            this.list_view.set_embedded_view(embedded);
 
4272
        }
 
4273
        this.list_view.m2m_field = this;
 
4274
        var loaded = $.Deferred();
 
4275
        this.list_view.on("list_view_loaded", this, function() {
 
4276
            loaded.resolve();
 
4277
        });
 
4278
        this.list_view.appendTo(this.$el);
 
4279
 
 
4280
        var old_def = self.is_loaded;
 
4281
        self.is_loaded = $.Deferred().done(function() {
 
4282
            old_def.resolve();
 
4283
        });
 
4284
        this.list_dm.add(loaded).then(function() {
 
4285
            self.is_loaded.resolve();
 
4286
        });
 
4287
    },
 
4288
    destroy_content: function() {
 
4289
        this.list_view.destroy();
 
4290
        this.list_view = undefined;
 
4291
    },
 
4292
    set_value: function(value_) {
 
4293
        value_ = value_ || [];
 
4294
        if (value_.length >= 1 && value_[0] instanceof Array) {
 
4295
            value_ = value_[0][2];
 
4296
        }
 
4297
        this._super(value_);
 
4298
    },
 
4299
    get_value: function() {
 
4300
        return [commands.replace_with(this.get('value'))];
 
4301
    },
 
4302
    is_false: function () {
 
4303
        return _(this.get("value")).isEmpty();
 
4304
    },
 
4305
    render_value: function() {
 
4306
        var self = this;
 
4307
        this.dataset.set_ids(this.get("value"));
 
4308
        this.render_value_dm.add(this.is_loaded).then(function() {
 
4309
            return self.list_view.reload_content();
 
4310
        });
 
4311
    },
 
4312
    dataset_changed: function() {
 
4313
        this.internal_set_value(this.dataset.ids);
 
4314
    },
 
4315
});
 
4316
 
 
4317
instance.web.form.Many2ManyDataSet = instance.web.DataSetStatic.extend({
 
4318
    get_context: function() {
 
4319
        this.context = this.m2m.build_context();
 
4320
        return this.context;
 
4321
    }
 
4322
});
 
4323
 
 
4324
/**
 
4325
 * @class
 
4326
 * @extends instance.web.ListView
 
4327
 */
 
4328
instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends instance.web.form.Many2ManyListView# */{
 
4329
    do_add_record: function () {
 
4330
        var pop = new instance.web.form.SelectCreatePopup(this);
 
4331
        pop.select_element(
 
4332
            this.model,
 
4333
            {
 
4334
                title: _t("Add: ") + this.m2m_field.string
 
4335
            },
 
4336
            new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]),
 
4337
            this.m2m_field.build_context()
 
4338
        );
 
4339
        var self = this;
 
4340
        pop.on("elements_selected", self, function(element_ids) {
 
4341
            var reload = false;
 
4342
            _(element_ids).each(function (id) {
 
4343
                if(! _.detect(self.dataset.ids, function(x) {return x == id;})) {
 
4344
                    self.dataset.set_ids(self.dataset.ids.concat([id]));
 
4345
                    self.m2m_field.dataset_changed();
 
4346
                    reload = true;
 
4347
                }
 
4348
            });
 
4349
            if (reload) {
 
4350
                self.reload_content();
 
4351
            }
 
4352
        });
 
4353
    },
 
4354
    do_activate_record: function(index, id) {
 
4355
        var self = this;
 
4356
        var pop = new instance.web.form.FormOpenPopup(this);
 
4357
        pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), {
 
4358
            title: _t("Open: ") + this.m2m_field.string,
 
4359
            readonly: this.getParent().get("effective_readonly")
 
4360
        });
 
4361
        pop.on('write_completed', self, self.reload_content);
 
4362
    },
 
4363
    do_button_action: function(name, id, callback) {
 
4364
        var self = this;
 
4365
        var _sup = _.bind(this._super, this);
 
4366
        if (! this.m2m_field.options.reload_on_button) {
 
4367
            return _sup(name, id, callback);
 
4368
        } else {
 
4369
            return this.m2m_field.view.save().then(function() {
 
4370
                return _sup(name, id, function() {
 
4371
                    self.m2m_field.view.reload();
 
4372
                });
 
4373
            });
 
4374
        }
 
4375
     },
 
4376
    is_action_enabled: function () { return true; },
 
4377
});
 
4378
 
 
4379
instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, {
 
4380
    disable_utility_classes: true,
 
4381
    init: function(field_manager, node) {
 
4382
        this._super(field_manager, node);
 
4383
        instance.web.form.CompletionFieldMixin.init.call(this);
 
4384
        m2m_kanban_lazy_init();
 
4385
        this.is_loaded = $.Deferred();
 
4386
        this.initial_is_loaded = this.is_loaded;
 
4387
 
 
4388
        var self = this;
 
4389
        this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation);
 
4390
        this.dataset.m2m = this;
 
4391
        this.dataset.on('unlink', self, function(ids) {
 
4392
            self.dataset_changed();
 
4393
        });
 
4394
    },
 
4395
    start: function() {
 
4396
        this._super.apply(this, arguments);
 
4397
 
 
4398
        var self = this;
 
4399
 
 
4400
        self.load_view();
 
4401
        self.on("change:effective_readonly", self, function() {
 
4402
            self.is_loaded = self.is_loaded.then(function() {
 
4403
                self.kanban_view.destroy();
 
4404
                return $.when(self.load_view()).done(function() {
 
4405
                    self.render_value();
 
4406
                });
 
4407
            });
 
4408
        });
 
4409
    },
 
4410
    set_value: function(value_) {
 
4411
        value_ = value_ || [];
 
4412
        if (value_.length >= 1 && value_[0] instanceof Array) {
 
4413
            value_ = value_[0][2];
 
4414
        }
 
4415
        this._super(value_);
 
4416
    },
 
4417
    get_value: function() {
 
4418
        return [commands.replace_with(this.get('value'))];
 
4419
    },
 
4420
    load_view: function() {
 
4421
        var self = this;
 
4422
        this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, {
 
4423
                    'create_text': _t("Add"),
 
4424
                    'creatable': self.get("effective_readonly") ? false : true,
 
4425
                    'quick_creatable': self.get("effective_readonly") ? false : true,
 
4426
                    'read_only_mode': self.get("effective_readonly") ? true : false,
 
4427
                    'confirm_on_delete': false,
 
4428
            });
 
4429
        var embedded = (this.field.views || {}).kanban;
 
4430
        if (embedded) {
 
4431
            this.kanban_view.set_embedded_view(embedded);
 
4432
        }
 
4433
        this.kanban_view.m2m = this;
 
4434
        var loaded = $.Deferred();
 
4435
        this.kanban_view.on("kanban_view_loaded",self,function() {
 
4436
            self.initial_is_loaded.resolve();
 
4437
            loaded.resolve();
 
4438
        });
 
4439
        this.kanban_view.on('switch_mode', this, this.open_popup);
 
4440
        $.async_when().done(function () {
 
4441
            self.kanban_view.appendTo(self.$el);
 
4442
        });
 
4443
        return loaded;
 
4444
    },
 
4445
    render_value: function() {
 
4446
        var self = this;
 
4447
        this.dataset.set_ids(this.get("value"));
 
4448
        this.is_loaded = this.is_loaded.then(function() {
 
4449
            return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []);
 
4450
        });
 
4451
    },
 
4452
    dataset_changed: function() {
 
4453
        this.set({'value': this.dataset.ids});
 
4454
    },
 
4455
    open_popup: function(type, unused) {
 
4456
        if (type !== "form")
 
4457
            return;
 
4458
        var self = this;
 
4459
        if (this.dataset.index === null) {
 
4460
            var pop = new instance.web.form.SelectCreatePopup(this);
 
4461
            pop.select_element(
 
4462
                this.field.relation,
 
4463
                {
 
4464
                    title: _t("Add: ") + this.string
 
4465
                },
 
4466
                new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]),
 
4467
                this.build_context()
 
4468
            );
 
4469
            pop.on("elements_selected", self, function(element_ids) {
 
4470
                _.each(element_ids, function(one_id) {
 
4471
                    if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) {
 
4472
                        self.dataset.set_ids([].concat(self.dataset.ids, [one_id]));
 
4473
                        self.dataset_changed();
 
4474
                        self.render_value();
 
4475
                    }
 
4476
                });
 
4477
            });
 
4478
        } else {
 
4479
            var id = self.dataset.ids[self.dataset.index];
 
4480
            var pop = new instance.web.form.FormOpenPopup(this);
 
4481
            pop.show_element(self.field.relation, id, self.build_context(), {
 
4482
                title: _t("Open: ") + self.string,
 
4483
                write_function: function(id, data, options) {
 
4484
                    return self.dataset.write(id, data, {}).done(function() {
 
4485
                        self.render_value();
 
4486
                    });
 
4487
                },
 
4488
                alternative_form_view: self.field.views ? self.field.views["form"] : undefined,
 
4489
                parent_view: self.view,
 
4490
                child_name: self.name,
 
4491
                readonly: self.get("effective_readonly")
 
4492
            });
 
4493
        }
 
4494
    },
 
4495
    add_id: function(id) {
 
4496
        this.quick_create.add_id(id);
 
4497
    },
 
4498
});
 
4499
 
 
4500
function m2m_kanban_lazy_init() {
 
4501
if (instance.web.form.Many2ManyKanbanView)
 
4502
    return;
 
4503
instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({
 
4504
    quick_create_class: 'instance.web.form.Many2ManyQuickCreate',
 
4505
    _is_quick_create_enabled: function() {
 
4506
        return this._super() && ! this.group_by;
 
4507
    },
 
4508
});
 
4509
instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({
 
4510
    template: 'Many2ManyKanban.quick_create',
 
4511
 
 
4512
    /**
 
4513
     * close_btn: If true, the widget will display a "Close" button able to trigger
 
4514
     * a "close" event.
 
4515
     */
 
4516
    init: function(parent, dataset, context, buttons) {
 
4517
        this._super(parent);
 
4518
        this.m2m = this.getParent().view.m2m;
 
4519
        this.m2m.quick_create = this;
 
4520
        this._dataset = dataset;
 
4521
        this._buttons = buttons || false;
 
4522
        this._context = context || {};
 
4523
    },
 
4524
    start: function () {
 
4525
        var self = this;
 
4526
        self.$text = this.$el.find('input').css("width", "200px");
 
4527
        self.$text.textext({
 
4528
            plugins : 'arrow autocomplete',
 
4529
            autocomplete: {
 
4530
                render: function(suggestion) {
 
4531
                    return $('<span class="text-label"/>').
 
4532
                             data('index', suggestion['index']).html(suggestion['label']);
 
4533
                }
 
4534
            },
 
4535
            ext: {
 
4536
                autocomplete: {
 
4537
                    selectFromDropdown: function() {
 
4538
                        $(this).trigger('hideDropdown');
 
4539
                        var index = Number(this.selectedSuggestionElement().children().children().data('index'));
 
4540
                        var data = self.search_result[index];
 
4541
                        if (data.id) {
 
4542
                            self.add_id(data.id);
 
4543
                        } else {
 
4544
                            data.action();
 
4545
                        }
 
4546
                    },
 
4547
                },
 
4548
                itemManager: {
 
4549
                    itemToString: function(item) {
 
4550
                        return item.name;
 
4551
                    },
 
4552
                },
 
4553
            },
 
4554
        }).bind('getSuggestions', function(e, data) {
 
4555
            var _this = this;
 
4556
            var str = !!data ? data.query || '' : '';
 
4557
            self.m2m.get_search_result(str).done(function(result) {
 
4558
                self.search_result = result;
 
4559
                $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) {
 
4560
                    return _.extend(el, {index:i});
 
4561
                })});
 
4562
            });
 
4563
        });
 
4564
        self.$text.focusout(function() {
 
4565
            self.$text.val("");
 
4566
        });
 
4567
    },
 
4568
    focus: function() {
 
4569
        this.$text[0].focus();
 
4570
    },
 
4571
    add_id: function(id) {
 
4572
        var self = this;
 
4573
        self.$text.val("");
 
4574
        self.trigger('added', id);
 
4575
        this.m2m.dataset_changed();
 
4576
    },
 
4577
});
 
4578
}
 
4579
 
 
4580
/**
 
4581
 * Class with everything which is common between FormOpenPopup and SelectCreatePopup.
 
4582
 */
 
4583
instance.web.form.AbstractFormPopup = instance.web.Widget.extend({
 
4584
    template: "AbstractFormPopup.render",
 
4585
    /**
 
4586
     *  options:
 
4587
     *  -readonly: only applicable when not in creation mode, default to false
 
4588
     * - alternative_form_view
 
4589
     * - view_id
 
4590
     * - write_function
 
4591
     * - read_function
 
4592
     * - create_function
 
4593
     * - parent_view
 
4594
     * - child_name
 
4595
     * - form_view_options
 
4596
     */
 
4597
    init_popup: function(model, row_id, domain, context, options) {
 
4598
        this.row_id = row_id;
 
4599
        this.model = model;
 
4600
        this.domain = domain || [];
 
4601
        this.context = context || {};
 
4602
        this.options = options;
 
4603
        _.defaults(this.options, {
 
4604
        });
 
4605
    },
 
4606
    init_dataset: function() {
 
4607
        var self = this;
 
4608
        this.created_elements = [];
 
4609
        this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context);
 
4610
        this.dataset.read_function = this.options.read_function;
 
4611
        this.dataset.create_function = function(data, options, sup) {
 
4612
            var fct = self.options.create_function || sup;
 
4613
            return fct.call(this, data, options).done(function(r) {
 
4614
                self.trigger('create_completed saved', r);
 
4615
                self.created_elements.push(r);
 
4616
            });
 
4617
        };
 
4618
        this.dataset.write_function = function(id, data, options, sup) {
 
4619
            var fct = self.options.write_function || sup;
 
4620
            return fct.call(this, id, data, options).done(function(r) {
 
4621
                self.trigger('write_completed saved', r);
 
4622
            });
 
4623
        };
 
4624
        this.dataset.parent_view = this.options.parent_view;
 
4625
        this.dataset.child_name = this.options.child_name;
 
4626
    },
 
4627
    display_popup: function() {
 
4628
        var self = this;
 
4629
        this.renderElement();
 
4630
        var dialog = new instance.web.Dialog(this, {
 
4631
            min_width: '800px',
 
4632
            dialogClass: 'oe_act_window',
 
4633
            close: function() {
 
4634
                self.check_exit(true);
 
4635
            },
 
4636
            title: this.options.title || "",
 
4637
        }, this.$el).open();
 
4638
        this.$buttonpane = dialog.$buttons;
 
4639
        this.start();
 
4640
    },
 
4641
    setup_form_view: function() {
 
4642
        var self = this;
 
4643
        if (this.row_id) {
 
4644
            this.dataset.ids = [this.row_id];
 
4645
            this.dataset.index = 0;
 
4646
        } else {
 
4647
            this.dataset.index = null;
 
4648
        }
 
4649
        var options = _.clone(self.options.form_view_options) || {};
 
4650
        if (this.row_id !== null) {
 
4651
            options.initial_mode = this.options.readonly ? "view" : "edit";
 
4652
        }
 
4653
        _.extend(options, {
 
4654
            $buttons: this.$buttonpane,
 
4655
        });
 
4656
        this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options);
 
4657
        if (this.options.alternative_form_view) {
 
4658
            this.view_form.set_embedded_view(this.options.alternative_form_view);
 
4659
        }
 
4660
        this.view_form.appendTo(this.$el.find(".oe_popup_form"));
 
4661
        this.view_form.on("form_view_loaded", self, function() {
 
4662
            var multi_select = self.row_id === null && ! self.options.disable_multiple_selection;
 
4663
            self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {
 
4664
                multi_select: multi_select,
 
4665
                readonly: self.row_id !== null && self.options.readonly,
 
4666
            }));
 
4667
            var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new");
 
4668
            $snbutton.click(function() {
 
4669
                $.when(self.view_form.save()).done(function() {
 
4670
                    self.view_form.reload_mutex.exec(function() {
 
4671
                        self.view_form.on_button_new();
 
4672
                    });
 
4673
                });
 
4674
            });
 
4675
            var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save");
 
4676
            $sbutton.click(function() {
 
4677
                $.when(self.view_form.save()).done(function() {
 
4678
                    self.view_form.reload_mutex.exec(function() {
 
4679
                        self.check_exit();
 
4680
                    });
 
4681
                });
 
4682
            });
 
4683
            var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close");
 
4684
            $cbutton.click(function() {
 
4685
                self.view_form.trigger('on_button_cancel');
 
4686
                self.check_exit();
 
4687
            });
 
4688
            self.view_form.do_show();
 
4689
        });
 
4690
    },
 
4691
    select_elements: function(element_ids) {
 
4692
        this.trigger("elements_selected", element_ids);
 
4693
    },
 
4694
    check_exit: function(no_destroy) {
 
4695
        if (this.created_elements.length > 0) {
 
4696
            this.select_elements(this.created_elements);
 
4697
            this.created_elements = [];
 
4698
        }
 
4699
        this.trigger('closed');
 
4700
        this.destroy();
 
4701
    },
 
4702
    destroy: function () {
 
4703
        this.trigger('closed');
 
4704
        if (this.$el.is(":data(dialog)")) {
 
4705
            this.$el.dialog('close');
 
4706
        }
 
4707
        this._super();
 
4708
    },
 
4709
});
 
4710
 
 
4711
/**
 
4712
 * Class to display a popup containing a form view.
 
4713
 */
 
4714
instance.web.form.FormOpenPopup = instance.web.form.AbstractFormPopup.extend({
 
4715
    show_element: function(model, row_id, context, options) {
 
4716
        this.init_popup(model, row_id, [], context,  options);
 
4717
        _.defaults(this.options, {
 
4718
        });
 
4719
        this.display_popup();
 
4720
    },
 
4721
    start: function() {
 
4722
        this._super();
 
4723
        this.init_dataset();
 
4724
        this.setup_form_view();
 
4725
    },
 
4726
});
 
4727
 
 
4728
/**
 
4729
 * Class to display a popup to display a list to search a row. It also allows
 
4730
 * to switch to a form view to create a new row.
 
4731
 */
 
4732
instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend({
 
4733
    /**
 
4734
     * options:
 
4735
     * - initial_ids
 
4736
     * - initial_view: form or search (default search)
 
4737
     * - disable_multiple_selection
 
4738
     * - list_view_options
 
4739
     */
 
4740
    select_element: function(model, options, domain, context) {
 
4741
        this.init_popup(model, null, domain, context, options);
 
4742
        var self = this;
 
4743
        _.defaults(this.options, {
 
4744
            initial_view: "search",
 
4745
        });
 
4746
        this.initial_ids = this.options.initial_ids;
 
4747
        this.display_popup();
 
4748
    },
 
4749
    start: function() {
 
4750
        var self = this;
 
4751
        this.init_dataset();
 
4752
        if (this.options.initial_view == "search") {
 
4753
            instance.web.pyeval.eval_domains_and_contexts({
 
4754
                domains: [],
 
4755
                contexts: [this.context]
 
4756
            }).done(function (results) {
 
4757
                var search_defaults = {};
 
4758
                _.each(results.context, function (value_, key) {
 
4759
                    var match = /^search_default_(.*)$/.exec(key);
 
4760
                    if (match) {
 
4761
                        search_defaults[match[1]] = value_;
 
4762
                    }
 
4763
                });
 
4764
                self.setup_search_view(search_defaults);
 
4765
            });
 
4766
        } else { // "form"
 
4767
            this.new_object();
 
4768
        }
 
4769
    },
 
4770
    setup_search_view: function(search_defaults) {
 
4771
        var self = this;
 
4772
        if (this.searchview) {
 
4773
            this.searchview.destroy();
 
4774
        }
 
4775
        this.searchview = new instance.web.SearchView(this,
 
4776
                this.dataset, false,  search_defaults);
 
4777
        this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
 
4778
            if (self.initial_ids) {
 
4779
                self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
 
4780
                    contexts, groupbys);
 
4781
                self.initial_ids = undefined;
 
4782
            } else {
 
4783
                self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
 
4784
            }
 
4785
        });
 
4786
        this.searchview.on("search_view_loaded", self, function() {
 
4787
            self.view_list = new instance.web.form.SelectCreateListView(self,
 
4788
                    self.dataset, false,
 
4789
                    _.extend({'deletable': false,
 
4790
                        'selectable': !self.options.disable_multiple_selection,
 
4791
                        'import_enabled': false,
 
4792
                        '$buttons': self.$buttonpane,
 
4793
                        'disable_editable_mode': true,
 
4794
                        '$pager': self.$('.oe_popup_list_pager'),
 
4795
                    }, self.options.list_view_options || {}));
 
4796
            self.view_list.on('edit:before', self, function (e) {
 
4797
                e.cancel = true;
 
4798
            });
 
4799
            self.view_list.popup = self;
 
4800
            self.view_list.appendTo($(".oe_popup_list", self.$el)).then(function() {
 
4801
                self.view_list.do_show();
 
4802
            }).then(function() {
 
4803
                self.searchview.do_search();
 
4804
            });
 
4805
            self.view_list.on("list_view_loaded", self, function() {
 
4806
                self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self}));
 
4807
                var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close");
 
4808
                $cbutton.click(function() {
 
4809
                    self.destroy();
 
4810
                });
 
4811
                var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select");
 
4812
                $sbutton.click(function() {
 
4813
                    self.select_elements(self.selected_ids);
 
4814
                    self.destroy();
 
4815
                });
 
4816
                var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create");
 
4817
                $cbutton.click(function() {
 
4818
                    self.new_object();
 
4819
                });
 
4820
            });
 
4821
        });
 
4822
        this.searchview.appendTo($(".oe_popup_search", self.$el));
 
4823
    },
 
4824
    do_search: function(domains, contexts, groupbys) {
 
4825
        var self = this;
 
4826
        instance.web.pyeval.eval_domains_and_contexts({
 
4827
            domains: domains || [],
 
4828
            contexts: contexts || [],
 
4829
            group_by_seq: groupbys || []
 
4830
        }).done(function (results) {
 
4831
            self.view_list.do_search(results.domain, results.context, results.group_by);
 
4832
        });
 
4833
    },
 
4834
    on_click_element: function(ids) {
 
4835
        var self = this;
 
4836
        this.selected_ids = ids || [];
 
4837
        if(this.selected_ids.length > 0) {
 
4838
            self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled');
 
4839
        } else {
 
4840
            self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled");
 
4841
        }
 
4842
    },
 
4843
    new_object: function() {
 
4844
        if (this.searchview) {
 
4845
            this.searchview.hide();
 
4846
        }
 
4847
        if (this.view_list) {
 
4848
            this.view_list.do_hide();
 
4849
        }
 
4850
        this.setup_form_view();
 
4851
    },
 
4852
});
 
4853
 
 
4854
instance.web.form.SelectCreateListView = instance.web.ListView.extend({
 
4855
    do_add_record: function () {
 
4856
        this.popup.new_object();
 
4857
    },
 
4858
    select_record: function(index) {
 
4859
        this.popup.select_elements([this.dataset.ids[index]]);
 
4860
        this.popup.destroy();
 
4861
    },
 
4862
    do_select: function(ids, records) {
 
4863
        this._super(ids, records);
 
4864
        this.popup.on_click_element(ids);
 
4865
    }
 
4866
});
 
4867
 
 
4868
instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 
4869
    template: 'FieldReference',
 
4870
    init: function(field_manager, node) {
 
4871
        this._super(field_manager, node);
 
4872
        this.reference_ready = true;
 
4873
    },
 
4874
    destroy_content: function() {
 
4875
        if (this.fm) {
 
4876
            this.fm.destroy();
 
4877
            this.fm = undefined;
 
4878
        }
 
4879
    },
 
4880
    initialize_content: function() {
 
4881
        var self = this;
 
4882
        var fm = new instance.web.form.DefaultFieldManager(this);
 
4883
        this.fm = fm;
 
4884
        fm.extend_field_desc({
 
4885
            "selection": {
 
4886
                selection: this.field_manager.get_field_desc(this.name).selection,
 
4887
                type: "selection",
 
4888
            },
 
4889
            "m2o": {
 
4890
                relation: null,
 
4891
                type: "many2one",
 
4892
            },
 
4893
        });
 
4894
        this.selection = new instance.web.form.FieldSelection(fm, { attrs: {
 
4895
            name: 'selection',
 
4896
            modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
 
4897
        }});
 
4898
        this.selection.on("change:value", this, this.on_selection_changed);
 
4899
        this.selection.appendTo(this.$(".oe_form_view_reference_selection"));
 
4900
        this.selection
 
4901
            .on('focused', null, function () {self.trigger('focused')})
 
4902
            .on('blurred', null, function () {self.trigger('blurred')});
 
4903
 
 
4904
        this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: {
 
4905
            name: 'm2o',
 
4906
            modifiers: JSON.stringify({readonly: this.get('effective_readonly')}),
 
4907
        }});
 
4908
        this.m2o.on("change:value", this, this.data_changed);
 
4909
        this.m2o.appendTo(this.$(".oe_form_view_reference_m2o"));
 
4910
        this.m2o
 
4911
            .on('focused', null, function () {self.trigger('focused')})
 
4912
            .on('blurred', null, function () {self.trigger('blurred')});
 
4913
    },
 
4914
    on_selection_changed: function() {
 
4915
        if (this.reference_ready) {
 
4916
            this.internal_set_value([this.selection.get_value(), false]);
 
4917
            this.render_value();
 
4918
        }
 
4919
    },
 
4920
    data_changed: function() {
 
4921
        if (this.reference_ready) {
 
4922
            this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]);
 
4923
        }
 
4924
    },
 
4925
    set_value: function(val) {
 
4926
        if (val) {
 
4927
            val = val.split(',');
 
4928
            val[0] = val[0] || false;
 
4929
            val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false;
 
4930
        }
 
4931
        this._super(val || [false, false]);
 
4932
    },
 
4933
    get_value: function() {
 
4934
        return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false;
 
4935
    },
 
4936
    render_value: function() {
 
4937
        this.reference_ready = false;
 
4938
        if (!this.get("effective_readonly")) {
 
4939
            this.selection.set_value(this.get('value')[0]);
 
4940
        }
 
4941
        this.m2o.field.relation = this.get('value')[0];
 
4942
        this.m2o.set_value(this.get('value')[1]);
 
4943
        this.m2o.$el.toggle(!!this.get('value')[0]);
 
4944
        this.reference_ready = true;
 
4945
    },
 
4946
});
 
4947
 
 
4948
instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, {
 
4949
    init: function(field_manager, node) {
 
4950
        var self = this;
 
4951
        this._super(field_manager, node);
 
4952
        this.binary_value = false;
 
4953
        this.useFileAPI = !!window.FileReader;
 
4954
        this.max_upload_size = 25 * 1024 * 1024; // 25Mo
 
4955
        if (!this.useFileAPI) {
 
4956
            this.fileupload_id = _.uniqueId('oe_fileupload');
 
4957
            $(window).on(this.fileupload_id, function() {
 
4958
                var args = [].slice.call(arguments).slice(1);
 
4959
                self.on_file_uploaded.apply(self, args);
 
4960
            });
 
4961
        }
 
4962
    },
 
4963
    stop: function() {
 
4964
        if (!this.useFileAPI) {
 
4965
            $(window).off(this.fileupload_id);
 
4966
        }
 
4967
        this._super.apply(this, arguments);
 
4968
    },
 
4969
    initialize_content: function() {
 
4970
        this.$el.find('input.oe_form_binary_file').change(this.on_file_change);
 
4971
        this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
 
4972
        this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
 
4973
    },
 
4974
    on_file_change: function(e) {
 
4975
        var self = this;
 
4976
        var file_node = e.target;
 
4977
        if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) {
 
4978
            if (this.useFileAPI) {
 
4979
                var file = file_node.files[0];
 
4980
                if (file.size > this.max_upload_size) {
 
4981
                    var msg = _t("The selected file exceed the maximum file size of %s.");
 
4982
                    instance.webclient.notification.warn(_t("File upload"), _.str.sprintf(msg, instance.web.human_size(this.max_upload_size)));
 
4983
                    return false;
 
4984
                }
 
4985
                var filereader = new FileReader();
 
4986
                filereader.readAsDataURL(file);
 
4987
                filereader.onloadend = function(upload) {
 
4988
                    var data = upload.target.result;
 
4989
                    data = data.split(',')[1];
 
4990
                    self.on_file_uploaded(file.size, file.name, file.type, data);
 
4991
                };
 
4992
            } else {
 
4993
                this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id);
 
4994
                this.$el.find('form.oe_form_binary_form').submit();
 
4995
            }
 
4996
            this.$el.find('.oe_form_binary_progress').show();
 
4997
            this.$el.find('.oe_form_binary').hide();
 
4998
        }
 
4999
    },
 
5000
    on_file_uploaded: function(size, name, content_type, file_base64) {
 
5001
        if (size === false) {
 
5002
            this.do_warn(_t("File Upload"), _t("There was a problem while uploading your file"));
 
5003
            // TODO: use openerp web crashmanager
 
5004
            console.warn("Error while uploading file : ", name);
 
5005
        } else {
 
5006
            this.filename = name;
 
5007
            this.on_file_uploaded_and_valid.apply(this, arguments);
 
5008
        }
 
5009
        this.$el.find('.oe_form_binary_progress').hide();
 
5010
        this.$el.find('.oe_form_binary').show();
 
5011
    },
 
5012
    on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
 
5013
    },
 
5014
    on_save_as: function(ev) {
 
5015
        var value = this.get('value');
 
5016
        if (!value) {
 
5017
            this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !"));
 
5018
            ev.stopPropagation();
 
5019
        } else {
 
5020
            instance.web.blockUI();
 
5021
            var c = instance.webclient.crashmanager;
 
5022
            this.session.get_file({
 
5023
                url: '/web/binary/saveas_ajax',
 
5024
                data: {data: JSON.stringify({
 
5025
                    model: this.view.dataset.model,
 
5026
                    id: (this.view.datarecord.id || ''),
 
5027
                    field: this.name,
 
5028
                    filename_field: (this.node.attrs.filename || ''),
 
5029
                    data: instance.web.form.is_bin_size(value) ? null : value,
 
5030
                    context: this.view.dataset.get_context()
 
5031
                })},
 
5032
                complete: instance.web.unblockUI,
 
5033
                error: c.rpc_error.bind(c)
 
5034
            });
 
5035
            ev.stopPropagation();
 
5036
            return false;
 
5037
        }
 
5038
    },
 
5039
    set_filename: function(value) {
 
5040
        var filename = this.node.attrs.filename;
 
5041
        if (filename) {
 
5042
            var tmp = {};
 
5043
            tmp[filename] = value;
 
5044
            this.field_manager.set_values(tmp);
 
5045
        }
 
5046
    },
 
5047
    on_clear: function() {
 
5048
        if (this.get('value') !== false) {
 
5049
            this.binary_value = false;
 
5050
            this.internal_set_value(false);
 
5051
        }
 
5052
        return false;
 
5053
    }
 
5054
});
 
5055
 
 
5056
instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
 
5057
    template: 'FieldBinaryFile',
 
5058
    initialize_content: function() {
 
5059
        this._super();
 
5060
        if (this.get("effective_readonly")) {
 
5061
            var self = this;
 
5062
            this.$el.find('a').click(function(ev) {
 
5063
                if (self.get('value')) {
 
5064
                    self.on_save_as(ev);
 
5065
                }
 
5066
                return false;
 
5067
            });
 
5068
        }
 
5069
    },
 
5070
    render_value: function() {
 
5071
        if (!this.get("effective_readonly")) {
 
5072
            var show_value;
 
5073
            if (this.node.attrs.filename) {
 
5074
                show_value = this.view.datarecord[this.node.attrs.filename] || '';
 
5075
            } else {
 
5076
                show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : '';
 
5077
            }
 
5078
            this.$el.find('input').eq(0).val(show_value);
 
5079
        } else {
 
5080
            this.$el.find('a').toggle(!!this.get('value'));
 
5081
            if (this.get('value')) {
 
5082
                var show_value = _t("Download")
 
5083
                if (this.view)
 
5084
                    show_value += " " + (this.view.datarecord[this.node.attrs.filename] || '');
 
5085
                this.$el.find('a').text(show_value);
 
5086
            }
 
5087
        }
 
5088
    },
 
5089
    on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
 
5090
        this.binary_value = true;
 
5091
        this.internal_set_value(file_base64);
 
5092
        var show_value = name + " (" + instance.web.human_size(size) + ")";
 
5093
        this.$el.find('input').eq(0).val(show_value);
 
5094
        this.set_filename(name);
 
5095
    },
 
5096
    on_clear: function() {
 
5097
        this._super.apply(this, arguments);
 
5098
        this.$el.find('input').eq(0).val('');
 
5099
        this.set_filename('');
 
5100
    }
 
5101
});
 
5102
 
 
5103
instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
 
5104
    template: 'FieldBinaryImage',
 
5105
    placeholder: "/web/static/src/img/placeholder.png",
 
5106
    render_value: function() {
 
5107
        var self = this;
 
5108
        var url;
 
5109
        if (this.get('value') && !instance.web.form.is_bin_size(this.get('value'))) {
 
5110
            url = 'data:image/png;base64,' + this.get('value');
 
5111
        } else if (this.get('value')) {
 
5112
            var id = JSON.stringify(this.view.datarecord.id || null);
 
5113
            var field = this.name;
 
5114
            if (this.options.preview_image)
 
5115
                field = this.options.preview_image;
 
5116
            url = this.session.url('/web/binary/image', {
 
5117
                                        model: this.view.dataset.model,
 
5118
                                        id: id,
 
5119
                                        field: field,
 
5120
                                        t: (new Date().getTime()),
 
5121
            });
 
5122
        } else {
 
5123
            url = this.placeholder;
 
5124
        }
 
5125
        var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url }));
 
5126
        this.$el.find('> img').remove();
 
5127
        this.$el.prepend($img);
 
5128
        $img.load(function() {
 
5129
            if (! self.options.size)
 
5130
                return;
 
5131
            $img.css("max-width", "" + self.options.size[0] + "px");
 
5132
            $img.css("max-height", "" + self.options.size[1] + "px");
 
5133
            $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px");
 
5134
            $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px");
 
5135
        });
 
5136
        $img.on('error', function() {
 
5137
            $img.attr('src', self.placeholder);
 
5138
            instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image."));
 
5139
        });
 
5140
    },
 
5141
    on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
 
5142
        this.internal_set_value(file_base64);
 
5143
        this.binary_value = true;
 
5144
        this.render_value();
 
5145
        this.set_filename(name);
 
5146
    },
 
5147
    on_clear: function() {
 
5148
        this._super.apply(this, arguments);
 
5149
        this.render_value();
 
5150
        this.set_filename('');
 
5151
    }
 
5152
});
 
5153
 
 
5154
/**
 
5155
 * Widget for (one2many field) to upload one or more file in same time and display in list.
 
5156
 * The user can delete his files.
 
5157
 * Options on attribute ; "blockui" {Boolean} block the UI or not
 
5158
 * during the file is uploading
 
5159
 */
 
5160
instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({
 
5161
    template: "FieldBinaryFileUploader",
 
5162
    init: function(field_manager, node) {
 
5163
        this._super(field_manager, node);
 
5164
        this.field_manager = field_manager;
 
5165
        this.node = node;
 
5166
        if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') {
 
5167
            throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string);
 
5168
        }
 
5169
        this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment');
 
5170
        this.fileupload_id = _.uniqueId('oe_fileupload_temp');
 
5171
        $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this));
 
5172
    },
 
5173
    start: function() {
 
5174
        this._super(this);
 
5175
        this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change );
 
5176
    },
 
5177
    set_value: function(value_) {
 
5178
        var value_ = value_ || [];
 
5179
        var self = this;
 
5180
        var ids = [];
 
5181
        _.each(value_, function(command) {
 
5182
            if (isNaN(command) && command.id == undefined) {
 
5183
                switch (command[0]) {
 
5184
                    case commands.CREATE:
 
5185
                        ids = ids.concat(command[2]);
 
5186
                        return;
 
5187
                    case commands.REPLACE_WITH:
 
5188
                        ids = ids.concat(command[2]);
 
5189
                        return;
 
5190
                    case commands.UPDATE:
 
5191
                        ids = ids.concat(command[2]);
 
5192
                        return;
 
5193
                    case commands.LINK_TO:
 
5194
                        ids = ids.concat(command[1]);
 
5195
                        return;
 
5196
                    case commands.DELETE:
 
5197
                        ids = _.filter(ids, function (id) { return id != command[1];});
 
5198
                        return;
 
5199
                    case commands.DELETE_ALL:
 
5200
                        ids = [];
 
5201
                        return;
 
5202
                }
 
5203
            } else {
 
5204
                ids.push(command);
 
5205
            }
 
5206
        });
 
5207
        this._super( ids );
 
5208
    },
 
5209
    get_value: function() {
 
5210
        return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); });
 
5211
    },
 
5212
    get_file_url: function (attachment) {
 
5213
        return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
 
5214
    },
 
5215
    read_name_values : function () {
 
5216
        var self = this;
 
5217
        // select the list of id for a get_name
 
5218
        var values = [];
 
5219
        _.each(this.get('value'), function (val) {
 
5220
            if (typeof val != 'object') {
 
5221
                values.push(val);
 
5222
            }
 
5223
        });
 
5224
        // send request for get_name
 
5225
        if (values.length) {
 
5226
            return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) {
 
5227
                _.each(datas, function (data) {
 
5228
                    data.no_unlink = true;
 
5229
                    data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id});
 
5230
 
 
5231
                    _.each(self.get('value'), function (val, key) {
 
5232
                        if(val == data.id) {
 
5233
                            self.get('value')[key] = data;
 
5234
                        }
 
5235
                    });
 
5236
                });
 
5237
            });
 
5238
        } else {
 
5239
            return $.when(this.get('value'));
 
5240
        }
 
5241
    },
 
5242
    render_value: function () {
 
5243
        var self = this;
 
5244
        this.read_name_values().then(function (datas) {
 
5245
 
 
5246
            var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self}));
 
5247
            render.on('click', '.oe_delete', _.bind(self.on_file_delete, self));
 
5248
            self.$('.oe_placeholder_files, .oe_attachments').replaceWith( render );
 
5249
 
 
5250
            // reinit input type file
 
5251
            var $input = self.$('input.oe_form_binary_file');
 
5252
            $input.after($input.clone(true)).remove();
 
5253
            self.$(".oe_fileupload").show();
 
5254
 
 
5255
        });
 
5256
    },
 
5257
    on_file_change: function (event) {
 
5258
        event.stopPropagation();
 
5259
        var self = this;
 
5260
        var $target = $(event.target);
 
5261
        if ($target.val() !== '') {
 
5262
 
 
5263
            var filename = $target.val().replace(/.*[\\\/]/,'');
 
5264
 
 
5265
            // if the files is currently uploded, don't send again
 
5266
            if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) {
 
5267
                return false;
 
5268
            }
 
5269
 
 
5270
            // block UI or not
 
5271
            if(this.node.attrs.blockui>0) {
 
5272
                instance.web.blockUI();
 
5273
            }
 
5274
 
 
5275
            // if the files exits for this answer, delete the file before upload
 
5276
            var files = _.filter(this.get('value'), function (file) {
 
5277
                if((file.filename || file.name) == filename) {
 
5278
                    self.ds_file.unlink([file.id]);
 
5279
                    return false;
 
5280
                } else {
 
5281
                    return true;
 
5282
                }
 
5283
            });
 
5284
 
 
5285
            // TODO : unactivate send on wizard and form
 
5286
 
 
5287
            // submit file
 
5288
            this.$('form.oe_form_binary_form').submit();
 
5289
            this.$(".oe_fileupload").hide();
 
5290
 
 
5291
            // add file on result
 
5292
            files.push({
 
5293
                'id': 0,
 
5294
                'name': filename,
 
5295
                'filename': filename,
 
5296
                'url': '',
 
5297
                'upload': true
 
5298
            });
 
5299
 
 
5300
            this.set({'value': files});
 
5301
        }
 
5302
    },
 
5303
    on_file_loaded: function (event, result) {
 
5304
        var files = this.get('value');
 
5305
 
 
5306
        // unblock UI
 
5307
        if(this.node.attrs.blockui>0) {
 
5308
            instance.web.unblockUI();
 
5309
        }
 
5310
 
 
5311
        // TODO : activate send on wizard and form
 
5312
 
 
5313
        if (result.error || !result.id ) {
 
5314
            this.do_warn( _t('Uploading Error'), result.error);
 
5315
            files = _.filter(files, function (val) { return !val.upload; });
 
5316
        } else {
 
5317
            for(var i in files){
 
5318
                if(files[i].filename == result.filename && files[i].upload) {
 
5319
                    files[i] = {
 
5320
                        'id': result.id,
 
5321
                        'name': result.name,
 
5322
                        'filename': result.filename,
 
5323
                        'url': this.get_file_url(result)
 
5324
                    };
 
5325
                }
 
5326
            }
 
5327
        }
 
5328
 
 
5329
        this.set({'value': files});
 
5330
        this.render_value()
 
5331
    },
 
5332
    on_file_delete: function (event) {
 
5333
        event.stopPropagation();
 
5334
        var file_id=$(event.target).data("id");
 
5335
        if (file_id) {
 
5336
            var files=[];
 
5337
            for(var i in this.get('value')){
 
5338
                if(file_id != this.get('value')[i].id){
 
5339
                    files.push(this.get('value')[i]);
 
5340
                }
 
5341
                else if(!this.get('value')[i].no_unlink) {
 
5342
                    this.ds_file.unlink([file_id]);
 
5343
                }
 
5344
            }
 
5345
            this.set({'value': files});
 
5346
        }
 
5347
    },
 
5348
});
 
5349
 
 
5350
instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
 
5351
    template: "FieldStatus",
 
5352
    init: function(field_manager, node) {
 
5353
        this._super(field_manager, node);
 
5354
        this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false;
 
5355
        this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false;
 
5356
        this.set({value: false});
 
5357
        this.selection = [];
 
5358
        this.set("selection", []);
 
5359
        this.selection_dm = new instance.web.DropMisordered();
 
5360
    },
 
5361
    start: function() {
 
5362
        this.field_manager.on("view_content_has_changed", this, this.calc_domain);
 
5363
        this.calc_domain();
 
5364
        this.on("change:value", this, this.get_selection);
 
5365
        this.on("change:evaluated_selection_domain", this, this.get_selection);
 
5366
        this.on("change:selection", this, function() {
 
5367
            this.selection = this.get("selection");
 
5368
            this.render_value();
 
5369
        });
 
5370
        this.get_selection();
 
5371
        if (this.options.clickable) {
 
5372
            this.$el.on('click','li',this.on_click_stage);
 
5373
        }
 
5374
        if (this.$el.parent().is('header')) {
 
5375
            this.$el.after('<div class="oe_clear"/>');
 
5376
        }
 
5377
        this._super();
 
5378
    },
 
5379
    set_value: function(value_) {
 
5380
        if (value_ instanceof Array) {
 
5381
            value_ = value_[0];
 
5382
        }
 
5383
        this._super(value_);
 
5384
    },
 
5385
    render_value: function() {
 
5386
        var self = this;
 
5387
        var content = QWeb.render("FieldStatus.content", {widget: self});
 
5388
        self.$el.html(content);
 
5389
        var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}");
 
5390
        var color = colors[self.get('value')];
 
5391
        if (color) {
 
5392
            self.$("oe_active").css("color", color);
 
5393
        }
 
5394
    },
 
5395
    calc_domain: function() {
 
5396
        var d = instance.web.pyeval.eval('domain', this.build_domain());
 
5397
        var domain = []; //if there is no domain defined, fetch all the records
 
5398
        
 
5399
        if (d.length) {
 
5400
            domain = ['|',['id', '=', this.get('value')]].concat(d);
 
5401
        }
 
5402
        
 
5403
        if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) {
 
5404
            this.set("evaluated_selection_domain", domain);
 
5405
        }
 
5406
    },
 
5407
    /** Get the selection and render it
 
5408
     *  selection: [[identifier, value_to_display], ...]
 
5409
     *  For selection fields: this is directly given by this.field.selection
 
5410
     *  For many2one fields:  perform a search on the relation of the many2one field
 
5411
     */
 
5412
    get_selection: function() {
 
5413
        var self = this;
 
5414
        var selection = [];
 
5415
 
 
5416
        var calculation = _.bind(function() {
 
5417
            if (this.field.type == "many2one") {
 
5418
                var domain = [];
 
5419
                var ds = new instance.web.DataSetSearch(this, this.field.relation,
 
5420
                    self.build_context(), this.get("evaluated_selection_domain"));
 
5421
                return ds.read_slice(['name'], {}).then(function (records) {
 
5422
                    for(var i = 0; i < records.length; i++) {
 
5423
                        selection.push([records[i].id, records[i].name]);
 
5424
                    }
 
5425
                });
 
5426
            } else {
 
5427
                // For field type selection filter values according to
 
5428
                // statusbar_visible attribute of the field. For example:
 
5429
                // statusbar_visible="draft,open".
 
5430
                var select = this.field.selection;
 
5431
                for(var i=0; i < select.length; i++) {
 
5432
                    var key = select[i][0];
 
5433
                    if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) {
 
5434
                        selection.push(select[i]);
 
5435
                    }
 
5436
                }
 
5437
                return $.when();
 
5438
            }
 
5439
        }, this);
 
5440
        this.selection_dm.add(calculation()).then(function () {
 
5441
            if (! _.isEqual(selection, self.get("selection"))) {
 
5442
                self.set("selection", selection);
 
5443
            }
 
5444
        });
 
5445
    },
 
5446
    on_click_stage: function (ev) {
 
5447
        var self = this;
 
5448
        var $li = $(ev.currentTarget);
 
5449
        var val = parseInt($li.data("id"));
 
5450
        if (val != self.get('value')) {
 
5451
            this.view.recursive_save().done(function() {
 
5452
                var change = {};
 
5453
                change[self.name] = val;
 
5454
                self.view.dataset.write(self.view.datarecord.id, change).done(function() {
 
5455
                    self.view.reload();
 
5456
                });
 
5457
            });
 
5458
        }
 
5459
    },
 
5460
});
 
5461
 
 
5462
instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
 
5463
    template: "FieldMonetary",
 
5464
    widget_class: 'oe_form_field_float oe_form_field_monetary',
 
5465
    init: function() {
 
5466
        this._super.apply(this, arguments);
 
5467
        this.set({"currency": false});
 
5468
        if (this.options.currency_field) {
 
5469
            this.field_manager.on("field_changed:" + this.options.currency_field, this, function() {
 
5470
                this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)});
 
5471
            });
 
5472
        }
 
5473
        this.on("change:currency", this, this.get_currency_info);
 
5474
        this.get_currency_info();
 
5475
        this.ci_dm = new instance.web.DropMisordered();
 
5476
    },
 
5477
    start: function() {
 
5478
        var tmp = this._super();
 
5479
        this.on("change:currency_info", this, this.reinitialize);
 
5480
        return tmp;
 
5481
    },
 
5482
    get_currency_info: function() {
 
5483
        var self = this;
 
5484
        if (this.get("currency") === false) {
 
5485
            this.set({"currency_info": null});
 
5486
            return;
 
5487
        }
 
5488
        return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"])
 
5489
            .filter([["id", "=", self.get("currency")]]).first())).then(function(res) {
 
5490
            self.set({"currency_info": res});
 
5491
        });
 
5492
    },
 
5493
    parse_value: function(val, def) {
 
5494
        return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
 
5495
    },
 
5496
    format_value: function(val, def) {
 
5497
        return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def);
 
5498
    },
 
5499
});
 
5500
 
 
5501
/**
 
5502
 * Registry of form fields, called by :js:`instance.web.FormView`.
 
5503
 *
 
5504
 * All referenced classes must implement FieldInterface. Those represent the classes whose instances
 
5505
 * will substitute to the <field> tags as defined in OpenERP's views.
 
5506
 */
 
5507
instance.web.form.widgets = new instance.web.Registry({
 
5508
    'char' : 'instance.web.form.FieldChar',
 
5509
    'id' : 'instance.web.form.FieldID',
 
5510
    'email' : 'instance.web.form.FieldEmail',
 
5511
    'url' : 'instance.web.form.FieldUrl',
 
5512
    'text' : 'instance.web.form.FieldText',
 
5513
    'html' : 'instance.web.form.FieldTextHtml',
 
5514
    'date' : 'instance.web.form.FieldDate',
 
5515
    'datetime' : 'instance.web.form.FieldDatetime',
 
5516
    'selection' : 'instance.web.form.FieldSelection',
 
5517
    'many2one' : 'instance.web.form.FieldMany2One',
 
5518
    'many2onebutton' : 'instance.web.form.Many2OneButton',
 
5519
    'many2many' : 'instance.web.form.FieldMany2Many',
 
5520
    'many2many_tags' : 'instance.web.form.FieldMany2ManyTags',
 
5521
    'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban',
 
5522
    'one2many' : 'instance.web.form.FieldOne2Many',
 
5523
    'one2many_list' : 'instance.web.form.FieldOne2Many',
 
5524
    'reference' : 'instance.web.form.FieldReference',
 
5525
    'boolean' : 'instance.web.form.FieldBoolean',
 
5526
    'float' : 'instance.web.form.FieldFloat',
 
5527
    'integer': 'instance.web.form.FieldFloat',
 
5528
    'float_time': 'instance.web.form.FieldFloat',
 
5529
    'progressbar': 'instance.web.form.FieldProgressBar',
 
5530
    'image': 'instance.web.form.FieldBinaryImage',
 
5531
    'binary': 'instance.web.form.FieldBinaryFile',
 
5532
    'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles',
 
5533
    'statusbar': 'instance.web.form.FieldStatus',
 
5534
    'monetary': 'instance.web.form.FieldMonetary',
 
5535
});
 
5536
 
 
5537
/**
 
5538
 * Registry of widgets usable in the form view that can substitute to any possible
 
5539
 * tags defined in OpenERP's form views.
 
5540
 *
 
5541
 * Every referenced class should extend FormWidget.
 
5542
 */
 
5543
instance.web.form.tags = new instance.web.Registry({
 
5544
    'button' : 'instance.web.form.WidgetButton',
 
5545
});
 
5546
 
 
5547
instance.web.form.custom_widgets = new instance.web.Registry({
 
5548
});
 
5549
 
 
5550
};
 
5551
 
 
5552
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: