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

« back to all changes in this revision

Viewing changes to web/addons/web/static/src/js/views.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
/*---------------------------------------------------------
 
2
 * OpenERP web library
 
3
 *---------------------------------------------------------*/
 
4
 
 
5
openerp.web.views = function(instance) {
 
6
var QWeb = instance.web.qweb,
 
7
    _t = instance.web._t;
 
8
 
 
9
instance.web.ActionManager = instance.web.Widget.extend({
 
10
    init: function(parent) {
 
11
        this._super(parent);
 
12
        this.inner_action = null;
 
13
        this.inner_widget = null;
 
14
        this.dialog = null;
 
15
        this.dialog_widget = null;
 
16
        this.breadcrumbs = [];
 
17
        this.on('history_back', this, function() {
 
18
            return this.history_back();
 
19
        });
 
20
    },
 
21
    start: function() {
 
22
        this._super.apply(this, arguments);
 
23
        this.$el.on('click', 'a.oe_breadcrumb_item', this.on_breadcrumb_clicked);
 
24
    },
 
25
    dialog_stop: function () {
 
26
        if (this.dialog) {
 
27
            this.dialog.destroy();
 
28
        }
 
29
        this.dialog = null;
 
30
    },
 
31
    /**
 
32
     * Add a new item to the breadcrumb
 
33
     *
 
34
     * If the title of an item is an array, the multiple title mode is in use.
 
35
     * (eg: a widget with multiple views might need to display a title for each view)
 
36
     * In multiple title mode, the show() callback can check the index it receives
 
37
     * in order to detect which of its titles has been clicked on by the user.
 
38
     *
 
39
     * @param {Object} item breadcrumb item
 
40
     * @param {Object} item.widget widget containing the view(s) to be added to the breadcrumb added
 
41
     * @param {Function} [item.show] triggered whenever the widget should be shown back
 
42
     * @param {Function} [item.hide] triggered whenever the widget should be shown hidden
 
43
     * @param {Function} [item.destroy] triggered whenever the widget should be destroyed
 
44
     * @param {String|Array} [item.title] title(s) of the view(s) to be displayed in the breadcrumb
 
45
     * @param {Function} [item.get_title] should return the title(s) of the view(s) to be displayed in the breadcrumb
 
46
     */
 
47
    push_breadcrumb: function(item) {
 
48
        var last = this.breadcrumbs.slice(-1)[0];
 
49
        if (last) {
 
50
            last.hide();
 
51
        }
 
52
        var item = _.extend({
 
53
            show: function(index) {
 
54
                this.widget.$el.show();
 
55
            },
 
56
            hide: function() {
 
57
                this.widget.$el.hide();
 
58
            },
 
59
            destroy: function() {
 
60
                this.widget.destroy();
 
61
            },
 
62
            get_title: function() {
 
63
                return this.title || this.widget.get('title');
 
64
            }
 
65
        }, item);
 
66
        item.id = _.uniqueId('breadcrumb_');
 
67
        this.breadcrumbs.push(item);
 
68
    },
 
69
    history_back: function() {
 
70
        var last = this.breadcrumbs.slice(-1)[0];
 
71
        if (!last) {
 
72
            return false;
 
73
        }
 
74
        var title = last.get_title();
 
75
        if (_.isArray(title) && title.length > 1) {
 
76
            return this.select_breadcrumb(this.breadcrumbs.length - 1, title.length - 2);
 
77
        } else if (this.breadcrumbs.length === 1) {
 
78
            // Only one single titled item in breadcrumb, most of the time you want to trigger back to home
 
79
            return false;
 
80
        } else {
 
81
            var prev = this.breadcrumbs[this.breadcrumbs.length - 2];
 
82
            title = prev.get_title();
 
83
            return this.select_breadcrumb(this.breadcrumbs.length - 2, _.isArray(title) ? title.length - 1 : undefined);
 
84
        }
 
85
    },
 
86
    on_breadcrumb_clicked: function(ev) {
 
87
        var $e = $(ev.target);
 
88
        var id = $e.data('id');
 
89
        var index;
 
90
        for (var i = this.breadcrumbs.length - 1; i >= 0; i--) {
 
91
            if (this.breadcrumbs[i].id == id) {
 
92
                index = i;
 
93
                break;
 
94
            }
 
95
        }
 
96
        var subindex = $e.parent().find('a.oe_breadcrumb_item[data-id=' + $e.data('id') + ']').index($e);
 
97
        this.select_breadcrumb(index, subindex);
 
98
    },
 
99
    select_breadcrumb: function(index, subindex) {
 
100
        var next_item = this.breadcrumbs[index + 1];
 
101
        if (next_item && next_item.on_reverse_breadcrumb) {
 
102
            next_item.on_reverse_breadcrumb(this.breadcrumbs[index].widget);
 
103
        }
 
104
        for (var i = this.breadcrumbs.length - 1; i >= 0; i--) {
 
105
            if (i > index) {
 
106
                if (this.remove_breadcrumb(i) === false) {
 
107
                    return false;
 
108
                }
 
109
            }
 
110
        }
 
111
        var item = this.breadcrumbs[index];
 
112
        item.show(subindex);
 
113
        this.inner_widget = item.widget;
 
114
        this.inner_action = item.action;
 
115
        return true;
 
116
    },
 
117
    clear_breadcrumbs: function() {
 
118
        for (var i = this.breadcrumbs.length - 1; i >= 0; i--) {
 
119
            if (this.remove_breadcrumb(0) === false) {
 
120
                break;
 
121
            }
 
122
        }
 
123
    },
 
124
    remove_breadcrumb: function(index) {
 
125
        var item = this.breadcrumbs.splice(index, 1)[0];
 
126
        if (item) {
 
127
            var dups = _.filter(this.breadcrumbs, function(it) {
 
128
                return item.widget === it.widget;
 
129
            });
 
130
            if (!dups.length) {
 
131
                if (this.getParent().has_uncommitted_changes()) {
 
132
                    this.inner_widget = item.widget;
 
133
                    this.inner_action = item.action;
 
134
                    this.breadcrumbs.splice(index, 0, item);
 
135
                    return false;
 
136
                } else {
 
137
                    item.destroy();
 
138
                }
 
139
            }
 
140
        }
 
141
        var last_widget = this.breadcrumbs.slice(-1)[0];
 
142
        if (last_widget) {
 
143
            this.inner_widget = last_widget.widget;
 
144
            this.inner_action = last_widget.action;
 
145
        }
 
146
    },
 
147
    get_title: function() {
 
148
        var titles = [];
 
149
        for (var i = 0; i < this.breadcrumbs.length; i += 1) {
 
150
            var item = this.breadcrumbs[i];
 
151
            var tit = item.get_title();
 
152
            if (item.hide_breadcrumb) {
 
153
                continue;
 
154
            }
 
155
            if (!_.isArray(tit)) {
 
156
                tit = [tit];
 
157
            }
 
158
            for (var j = 0; j < tit.length; j += 1) {
 
159
                var label = _.escape(tit[j]);
 
160
                if (i === this.breadcrumbs.length - 1 && j === tit.length - 1) {
 
161
                    titles.push(_.str.sprintf('<span class="oe_breadcrumb_item">%s</span>', label));
 
162
                } else {
 
163
                    titles.push(_.str.sprintf('<a href="#" class="oe_breadcrumb_item" data-id="%s">%s</a>', item.id, label));
 
164
                }
 
165
            }
 
166
        }
 
167
        return titles.join(' <span class="oe_fade">/</span> ');
 
168
    },
 
169
    do_push_state: function(state) {
 
170
        state = state || {};
 
171
        if (this.getParent() && this.getParent().do_push_state) {
 
172
            if (this.inner_action) {
 
173
                if (this.inner_action._push_me === false) {
 
174
                    // this action has been explicitly marked as not pushable
 
175
                    return;
 
176
                }
 
177
                state['title'] = this.inner_action.name;
 
178
                if(this.inner_action.type == 'ir.actions.act_window') {
 
179
                    state['model'] = this.inner_action.res_model;
 
180
                }
 
181
                if (this.inner_action.menu_id) {
 
182
                    state['menu_id'] = this.inner_action.menu_id;
 
183
                }
 
184
                if (this.inner_action.id) {
 
185
                    state['action'] = this.inner_action.id;
 
186
                } else if (this.inner_action.type == 'ir.actions.client') {
 
187
                    state['action'] = this.inner_action.tag;
 
188
                    var params = {};
 
189
                    _.each(this.inner_action.params, function(v, k) {
 
190
                        if(_.isString(v) || _.isNumber(v)) {
 
191
                            params[k] = v;
 
192
                        }
 
193
                    });
 
194
                    state = _.extend(params || {}, state);
 
195
                }
 
196
                if (this.inner_action.context) {
 
197
                    var active_id = this.inner_action.context.active_id;
 
198
                    if (active_id) {
 
199
                        state["active_id"] = active_id;
 
200
                    }
 
201
                    var active_ids = this.inner_action.context.active_ids;
 
202
                    if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
 
203
                        // We don't push active_ids if it's a single element array containing the active_id
 
204
                        // This makes the url shorter in most cases.
 
205
                        state["active_ids"] = this.inner_action.context.active_ids.join(',');
 
206
                    }
 
207
                }
 
208
            }
 
209
            if(!this.dialog) {
 
210
                this.getParent().do_push_state(state);
 
211
            }
 
212
        }
 
213
    },
 
214
    do_load_state: function(state, warm) {
 
215
        var self = this,
 
216
            action_loaded;
 
217
        if (state.action) {
 
218
            if (_.isString(state.action) && instance.web.client_actions.contains(state.action)) {
 
219
                var action_client = {
 
220
                    type: "ir.actions.client",
 
221
                    tag: state.action,
 
222
                    params: state,
 
223
                    _push_me: state._push_me,
 
224
                };
 
225
                this.null_action();
 
226
                action_loaded = this.do_action(action_client);
 
227
            } else {
 
228
                var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action;
 
229
                if (run_action) {
 
230
                    var add_context = {};
 
231
                    if (state.active_id) {
 
232
                        add_context.active_id = state.active_id;
 
233
                    }
 
234
                    if (state.active_ids) {
 
235
                        // The jQuery BBQ plugin does some parsing on values that are valid integers.
 
236
                        // It means that if there's only one item, it will do parseInt() on it,
 
237
                        // otherwise it will keep the comma seperated list as string.
 
238
                        add_context.active_ids = state.active_ids.toString().split(',').map(function(id) {
 
239
                            return parseInt(id, 10) || id;
 
240
                        });
 
241
                    } else if (state.active_id) {
 
242
                        add_context.active_ids = [state.active_id];
 
243
                    }
 
244
                    this.null_action();
 
245
                    action_loaded = this.do_action(state.action, { additional_context: add_context });
 
246
                    $.when(action_loaded || null).done(function() {
 
247
                        instance.webclient.menu.has_been_loaded.done(function() {
 
248
                            if (self.inner_action && self.inner_action.id) {
 
249
                                instance.webclient.menu.open_action(self.inner_action.id);
 
250
                            }
 
251
                        });
 
252
                    });
 
253
                }
 
254
            }
 
255
        } else if (state.model && state.id) {
 
256
            // TODO handle context & domain ?
 
257
            this.null_action();
 
258
            var action = {
 
259
                res_model: state.model,
 
260
                res_id: state.id,
 
261
                type: 'ir.actions.act_window',
 
262
                views: [[false, 'form']]
 
263
            };
 
264
            action_loaded = this.do_action(action);
 
265
        } else if (state.sa) {
 
266
            // load session action
 
267
            this.null_action();
 
268
            action_loaded = this.rpc('/web/session/get_session_action',  {key: state.sa}).then(function(action) {
 
269
                if (action) {
 
270
                    return self.do_action(action);
 
271
                }
 
272
            });
 
273
        }
 
274
 
 
275
        $.when(action_loaded || null).done(function() {
 
276
            if (self.inner_widget && self.inner_widget.do_load_state) {
 
277
                self.inner_widget.do_load_state(state, warm);
 
278
            }
 
279
        });
 
280
    },
 
281
    /**
 
282
     * Execute an OpenERP action
 
283
     *
 
284
     * @param {Number|String|Object} Can be either an action id, a client action or an action descriptor.
 
285
     * @param {Object} [options]
 
286
     * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
 
287
     * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
 
288
     * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
 
289
     * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
 
290
     * @param {Function} [options.action_menu_id] Manually set the menu id on the fly.
 
291
     * @param {Object} [options.additional_context] Additional context to be merged with the action's context.
 
292
     * @return {jQuery.Deferred} Action loaded
 
293
     */
 
294
    do_action: function(action, options) {
 
295
        options = _.defaults(options || {}, {
 
296
            clear_breadcrumbs: false,
 
297
            on_reverse_breadcrumb: function() {},
 
298
            hide_breadcrumb: false,
 
299
            on_close: function() {},
 
300
            action_menu_id: null,
 
301
            additional_context: {},
 
302
        });
 
303
        if (action === false) {
 
304
            action = { type: 'ir.actions.act_window_close' };
 
305
        } else if (_.isString(action) && instance.web.client_actions.contains(action)) {
 
306
            var action_client = { type: "ir.actions.client", tag: action, params: {} };
 
307
            return this.do_action(action_client, options);
 
308
        } else if (_.isNumber(action) || _.isString(action)) {
 
309
            var self = this;
 
310
            return self.rpc("/web/action/load", { action_id: action }).then(function(result) {
 
311
                return self.do_action(result, options);
 
312
            });
 
313
        }
 
314
 
 
315
        // Ensure context & domain are evaluated and can be manipulated/used
 
316
        var ncontext = new instance.web.CompoundContext(options.additional_context, action.context || {});
 
317
        action.context = instance.web.pyeval.eval('context', ncontext);
 
318
        if (action.context.active_id || action.context.active_ids) {
 
319
            // Here we assume that when an `active_id` or `active_ids` is used
 
320
            // in the context, we are in a `related` action, so we disable the
 
321
            // searchview's default custom filters.
 
322
            action.context.search_disable_custom_filters = true;
 
323
        }
 
324
        if (action.domain) {
 
325
            action.domain = instance.web.pyeval.eval(
 
326
                'domain', action.domain, action.context || {});
 
327
        }
 
328
 
 
329
        if (!action.type) {
 
330
            console.error("No type for action", action);
 
331
            return $.Deferred().reject();
 
332
        }
 
333
        var type = action.type.replace(/\./g,'_');
 
334
        var popup = action.target === 'new';
 
335
        var inline = action.target === 'inline' || action.target === 'inlineview';
 
336
        action.flags = _.defaults(action.flags || {}, {
 
337
            views_switcher : !popup && !inline,
 
338
            search_view : !popup && !inline,
 
339
            action_buttons : !popup && !inline,
 
340
            sidebar : !popup && !inline,
 
341
            pager : !popup && !inline,
 
342
            display_title : !popup,
 
343
            search_disable_custom_filters: action.context && action.context.search_disable_custom_filters
 
344
        });
 
345
        action.menu_id = options.action_menu_id;
 
346
        if (!(type in this)) {
 
347
            console.error("Action manager can't handle action of type " + action.type, action);
 
348
            return $.Deferred().reject();
 
349
        }
 
350
        return this[type](action, options);
 
351
    },
 
352
    null_action: function() {
 
353
        this.dialog_stop();
 
354
        this.clear_breadcrumbs();
 
355
    },
 
356
    /**
 
357
     *
 
358
     * @param {Object} executor
 
359
     * @param {Object} executor.action original action
 
360
     * @param {Function<instance.web.Widget>} executor.widget function used to fetch the widget instance
 
361
     * @param {String} executor.klass CSS class to add on the dialog root, if action.target=new
 
362
     * @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget
 
363
     * @param {Object} options
 
364
     * @return {*}
 
365
     */
 
366
    ir_actions_common: function(executor, options) {
 
367
        if (this.inner_widget && executor.action.target !== 'new') {
 
368
            if (this.getParent().has_uncommitted_changes()) {
 
369
                return $.Deferred().reject();
 
370
            } else if (options.clear_breadcrumbs) {
 
371
                this.clear_breadcrumbs();
 
372
            }
 
373
        }
 
374
        var widget = executor.widget();
 
375
        if (executor.action.target === 'new') {
 
376
            if (this.dialog_widget && !this.dialog_widget.isDestroyed()) {
 
377
                this.dialog_widget.destroy();
 
378
            }
 
379
            this.dialog_stop();
 
380
            this.dialog = new instance.web.Dialog(this, {
 
381
                dialogClass: executor.klass,
 
382
            });
 
383
            this.dialog.on("closing", null, options.on_close);
 
384
            this.dialog.dialog_title = executor.action.name;
 
385
            if (widget instanceof instance.web.ViewManager) {
 
386
                _.extend(widget.flags, {
 
387
                    $buttons: this.dialog.$buttons,
 
388
                    footer_to_buttons: true,
 
389
                });
 
390
            }
 
391
            this.dialog_widget = widget;
 
392
            this.dialog_widget.setParent(this.dialog);
 
393
            var initialized = this.dialog_widget.appendTo(this.dialog.$el);
 
394
            this.dialog.open();
 
395
            return initialized;
 
396
        } else  {
 
397
            this.dialog_stop();
 
398
            this.inner_action = executor.action;
 
399
            this.inner_widget = widget;
 
400
            executor.post_process(widget);
 
401
            return this.inner_widget.appendTo(this.$el);
 
402
        }
 
403
    },
 
404
    ir_actions_act_window: function (action, options) {
 
405
        var self = this;
 
406
 
 
407
        return this.ir_actions_common({
 
408
            widget: function () { return new instance.web.ViewManagerAction(self, action); },
 
409
            action: action,
 
410
            klass: 'oe_act_window',
 
411
            post_process: function (widget) {
 
412
                widget.add_breadcrumb({
 
413
                    on_reverse_breadcrumb: options.on_reverse_breadcrumb,
 
414
                    hide_breadcrumb: options.hide_breadcrumb,
 
415
                });
 
416
            },
 
417
        }, options);
 
418
    },
 
419
    ir_actions_client: function (action, options) {
 
420
        var self = this;
 
421
        var ClientWidget = instance.web.client_actions.get_object(action.tag);
 
422
 
 
423
        if (!(ClientWidget.prototype instanceof instance.web.Widget)) {
 
424
            var next;
 
425
            if (next = ClientWidget(this, action)) {
 
426
                return this.do_action(next, options);
 
427
            }
 
428
            return $.when();
 
429
        }
 
430
 
 
431
        return this.ir_actions_common({
 
432
            widget: function () { return new ClientWidget(self, action); },
 
433
            action: action,
 
434
            klass: 'oe_act_client',
 
435
            post_process: function(widget) {
 
436
                self.push_breadcrumb({
 
437
                    widget: widget,
 
438
                    title: action.name,
 
439
                    on_reverse_breadcrumb: options.on_reverse_breadcrumb,
 
440
                    hide_breadcrumb: options.hide_breadcrumb,
 
441
                });
 
442
                if (action.tag !== 'reload') {
 
443
                    self.do_push_state({});
 
444
                }
 
445
            }
 
446
        }, options);
 
447
    },
 
448
    ir_actions_act_window_close: function (action, options) {
 
449
        if (!this.dialog) {
 
450
            options.on_close();
 
451
        }
 
452
        this.dialog_stop();
 
453
        return $.when();
 
454
    },
 
455
    ir_actions_server: function (action, options) {
 
456
        var self = this;
 
457
        this.rpc('/web/action/run', {
 
458
            action_id: action.id,
 
459
            context: action.context || {}
 
460
        }).done(function (action) {
 
461
            self.do_action(action, options)
 
462
        });
 
463
    },
 
464
    ir_actions_report_xml: function(action, options) {
 
465
        var self = this;
 
466
        instance.web.blockUI();
 
467
        return instance.web.pyeval.eval_domains_and_contexts({
 
468
            contexts: [action.context],
 
469
            domains: []
 
470
        }).then(function(res) {
 
471
            action = _.clone(action);
 
472
            action.context = res.context;
 
473
 
 
474
            // iOS devices doesn't allow iframe use the way we do it,
 
475
            // opening a new window seems the best way to workaround
 
476
            if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
 
477
                var params = {
 
478
                    action: JSON.stringify(action),
 
479
                    token: new Date().getTime()
 
480
                }
 
481
                var url = self.session.url('/web/report', params)
 
482
                instance.web.unblockUI();
 
483
                $('<a href="'+url+'" target="_blank"></a>')[0].click();
 
484
                return;
 
485
            }
 
486
 
 
487
            var c = instance.webclient.crashmanager;
 
488
            return $.Deferred(function (d) {
 
489
                self.session.get_file({
 
490
                    url: '/web/report',
 
491
                    data: {action: JSON.stringify(action)},
 
492
                    complete: instance.web.unblockUI,
 
493
                    success: function(){
 
494
                        if (!self.dialog) {
 
495
                            options.on_close();
 
496
                        }
 
497
                        self.dialog_stop();
 
498
                        d.resolve();
 
499
                    },
 
500
                    error: function () {
 
501
                        c.rpc_error.apply(c, arguments);
 
502
                        d.reject();
 
503
                    }
 
504
                })
 
505
            });
 
506
        });
 
507
    },
 
508
    ir_actions_act_url: function (action) {
 
509
        window.open(action.url, action.target === 'self' ? '_self' : '_blank');
 
510
        return $.when();
 
511
    },
 
512
});
 
513
 
 
514
instance.web.ViewManager =  instance.web.Widget.extend({
 
515
    template: "ViewManager",
 
516
    init: function(parent, dataset, views, flags) {
 
517
        this._super(parent);
 
518
        this.url_states = {};
 
519
        this.model = dataset ? dataset.model : undefined;
 
520
        this.dataset = dataset;
 
521
        this.searchview = null;
 
522
        this.active_view = null;
 
523
        this.views_src = _.map(views, function(x) {
 
524
            if (x instanceof Array) {
 
525
                var view_type = x[1];
 
526
                var View = instance.web.views.get_object(view_type, true);
 
527
                var view_label = View ? View.prototype.display_name : (void 'nope');
 
528
                return {
 
529
                    view_id: x[0],
 
530
                    view_type: view_type,
 
531
                    label: view_label,
 
532
                    button_label: View ? _.str.sprintf(_t('%(view_type)s view'), {'view_type': (view_label || view_type)}) : (void 'nope'),
 
533
                };
 
534
            } else {
 
535
                return x;
 
536
            }
 
537
        });
 
538
        this.ActionManager = parent;
 
539
        this.views = {};
 
540
        this.flags = flags || {};
 
541
        this.registry = instance.web.views;
 
542
        this.views_history = [];
 
543
        this.view_completely_inited = $.Deferred();
 
544
    },
 
545
    /**
 
546
     * @returns {jQuery.Deferred} initial view loading promise
 
547
     */
 
548
    start: function() {
 
549
        this._super();
 
550
        var self = this;
 
551
        this.$el.find('.oe_view_manager_switch a').click(function() {
 
552
            self.switch_mode($(this).data('view-type'));
 
553
        }).tipsy();
 
554
        var views_ids = {};
 
555
        _.each(this.views_src, function(view) {
 
556
            self.views[view.view_type] = $.extend({}, view, {
 
557
                deferred : $.Deferred(),
 
558
                controller : null,
 
559
                options : _.extend({
 
560
                    $buttons : self.$el.find('.oe_view_manager_buttons'),
 
561
                    $sidebar : self.flags.sidebar ? self.$el.find('.oe_view_manager_sidebar') : undefined,
 
562
                    $pager : self.$el.find('.oe_view_manager_pager'),
 
563
                    action : self.action,
 
564
                    action_views_ids : views_ids
 
565
                }, self.flags, self.flags[view.view_type] || {}, view.options || {})
 
566
            });
 
567
            views_ids[view.view_type] = view.view_id;
 
568
        });
 
569
        if (this.flags.views_switcher === false) {
 
570
            this.$el.find('.oe_view_manager_switch').hide();
 
571
        }
 
572
        // If no default view defined, switch to the first one in sequence
 
573
        var default_view = this.flags.default_view || this.views_src[0].view_type;
 
574
        return this.switch_mode(default_view);
 
575
    },
 
576
    switch_mode: function(view_type, no_store, view_options) {
 
577
        var self = this;
 
578
        var view = this.views[view_type];
 
579
        var view_promise;
 
580
        var form = this.views['form'];
 
581
        if (!view || (form && form.controller && !form.controller.can_be_discarded())) {
 
582
            self.trigger('switch_mode', view_type, no_store, view_options);
 
583
            return $.Deferred().reject();
 
584
        }
 
585
        if (!no_store) {
 
586
            this.views_history.push(view_type);
 
587
        }
 
588
        this.active_view = view_type;
 
589
 
 
590
        if (!view.controller) {
 
591
            view_promise = this.do_create_view(view_type);
 
592
        } else if (this.searchview
 
593
                && self.flags.auto_search
 
594
                && view.controller.searchable !== false) {
 
595
            this.searchview.ready.done(this.searchview.do_search);
 
596
        }
 
597
 
 
598
        if (this.searchview) {
 
599
            this.searchview[(view.controller.searchable === false || this.searchview.options.hidden) ? 'hide' : 'show']();
 
600
        }
 
601
 
 
602
        this.$el.find('.oe_view_manager_switch a').parent().removeClass('active');
 
603
        this.$el
 
604
            .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
 
605
            .parent().addClass('active');
 
606
 
 
607
        return $.when(view_promise).done(function () {
 
608
            _.each(_.keys(self.views), function(view_name) {
 
609
                var controller = self.views[view_name].controller;
 
610
                if (controller) {
 
611
                    var container = self.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_name);
 
612
                    if (view_name === view_type) {
 
613
                        container.show();
 
614
                        controller.do_show(view_options || {});
 
615
                    } else {
 
616
                        container.hide();
 
617
                        controller.do_hide();
 
618
                    }
 
619
                }
 
620
            });
 
621
            self.trigger('switch_mode', view_type, no_store, view_options);
 
622
        });
 
623
    },
 
624
    do_create_view: function(view_type) {
 
625
        // Lazy loading of views
 
626
        var self = this;
 
627
        var view = this.views[view_type];
 
628
        var viewclass = this.registry.get_object(view_type);
 
629
        var options = _.clone(view.options);
 
630
        if (view_type === "form" && this.action && (this.action.target == 'new' || this.action.target == 'inline')) {
 
631
            options.initial_mode = 'edit';
 
632
        }
 
633
        var controller = new viewclass(this, this.dataset, view.view_id, options);
 
634
 
 
635
        controller.on('history_back', this, function() {
 
636
            var am = self.getParent();
 
637
            if (am && am.trigger) {
 
638
                return am.trigger('history_back');
 
639
            }
 
640
        });
 
641
 
 
642
        controller.on("change:title", this, function() {
 
643
            if (self.active_view === view_type) {
 
644
                self.set_title(controller.get('title'));
 
645
            }
 
646
        });
 
647
 
 
648
        if (view.embedded_view) {
 
649
            controller.set_embedded_view(view.embedded_view);
 
650
        }
 
651
        controller.on('switch_mode', self, this.switch_mode);
 
652
        controller.on('previous_view', self, this.prev_view);
 
653
        
 
654
        var container = this.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_type);
 
655
        var view_promise = controller.appendTo(container);
 
656
        this.views[view_type].controller = controller;
 
657
        this.views[view_type].deferred.resolve(view_type);
 
658
        return $.when(view_promise).done(function() {
 
659
            if (self.searchview
 
660
                    && self.flags.auto_search
 
661
                    && view.controller.searchable !== false) {
 
662
                self.searchview.ready.done(self.searchview.do_search);
 
663
            } else {
 
664
                self.view_completely_inited.resolve();
 
665
            }
 
666
            self.trigger("controller_inited",view_type,controller);
 
667
        });
 
668
    },
 
669
    /**
 
670
     * @returns {Number|Boolean} the view id of the given type, false if not found
 
671
     */
 
672
    get_view_id: function(view_type) {
 
673
        return this.views[view_type] && this.views[view_type].view_id || false;
 
674
    },
 
675
    set_title: function(title) {
 
676
        this.$el.find('.oe_view_title_text:first').text(title);
 
677
    },
 
678
    add_breadcrumb: function(options) {
 
679
        var options = options || {};
 
680
        // 7.0 backward compatibility
 
681
        if (typeof options == 'function') {
 
682
            options = {
 
683
                on_reverse_breadcrumb: options
 
684
            };
 
685
        }
 
686
        // end of 7.0 backward compatibility
 
687
        var self = this;
 
688
        var views = [this.active_view || this.views_src[0].view_type];
 
689
        this.on('switch_mode', self, function(mode) {
 
690
            var last = views.slice(-1)[0];
 
691
            if (mode !== last) {
 
692
                if (mode !== 'form') {
 
693
                    views.length = 0;
 
694
                }
 
695
                views.push(mode);
 
696
            }
 
697
        });
 
698
        var item = _.extend({
 
699
            widget: this,
 
700
            action: this.action,
 
701
            show: function(index) {
 
702
                var view_to_select = views[index];
 
703
                var state = self.url_states[view_to_select];
 
704
                self.do_push_state(state || {});
 
705
                $.when(self.switch_mode(view_to_select)).done(function() {
 
706
                    self.$el.show();
 
707
                });
 
708
            },
 
709
            get_title: function() {
 
710
                var id;
 
711
                var currentIndex;
 
712
                _.each(self.getParent().breadcrumbs, function(bc, i) {
 
713
                    if (bc.widget === self) {
 
714
                        currentIndex = i;
 
715
                    }
 
716
                });
 
717
                var next = self.getParent().breadcrumbs.slice(currentIndex + 1)[0];
 
718
                var titles = _.map(views, function(v) {
 
719
                    var controller = self.views[v].controller;
 
720
                    if (v === 'form') {
 
721
                        id = controller.datarecord.id;
 
722
                    }
 
723
                    return controller.get('title');
 
724
                });
 
725
                if (next && next.action && next.action.res_id && self.dataset &&
 
726
                    self.active_view === 'form' && self.dataset.model === next.action.res_model && id === next.action.res_id) {
 
727
                    // If the current active view is a formview and the next item in the breadcrumbs
 
728
                    // is an action on same object (model / res_id), then we omit the current formview's title
 
729
                    titles.pop();
 
730
                }
 
731
                return titles;
 
732
            }
 
733
        }, options);
 
734
        this.getParent().push_breadcrumb(item);
 
735
    },
 
736
    /**
 
737
     * Returns to the view preceding the caller view in this manager's
 
738
     * navigation history (the navigation history is appended to via
 
739
     * switch_mode)
 
740
     *
 
741
     * @param {Object} [options]
 
742
     * @param {Boolean} [options.created=false] resource was created
 
743
     * @param {String} [options.default=null] view to switch to if no previous view
 
744
     * @returns {$.Deferred} switching end signal
 
745
     */
 
746
    prev_view: function (options) {
 
747
        options = options || {};
 
748
        var current_view = this.views_history.pop();
 
749
        var previous_view = this.views_history[this.views_history.length - 1] || options['default'];
 
750
        if (options.created && current_view === 'form' && previous_view === 'list') {
 
751
            // APR special case: "If creation mode from list (and only from a list),
 
752
            // after saving, go to page view (don't come back in list)"
 
753
            return this.switch_mode('form');
 
754
        } else if (options.created && !previous_view && this.action && this.action.flags.default_view === 'form') {
 
755
            // APR special case: "If creation from dashboard, we have no previous view
 
756
            return this.switch_mode('form');
 
757
        }
 
758
        return this.switch_mode(previous_view, true);
 
759
    },
 
760
    /**
 
761
     * Sets up the current viewmanager's search view.
 
762
     *
 
763
     * @param {Number|false} view_id the view to use or false for a default one
 
764
     * @returns {jQuery.Deferred} search view startup deferred
 
765
     */
 
766
    setup_search_view: function(view_id, search_defaults) {
 
767
        var self = this;
 
768
        if (this.searchview) {
 
769
            this.searchview.destroy();
 
770
        }
 
771
        var options = {
 
772
            hidden: this.flags.search_view === false,
 
773
            disable_custom_filters: this.flags.search_disable_custom_filters,
 
774
        };
 
775
        this.searchview = new instance.web.SearchView(this, this.dataset, view_id, search_defaults, options);
 
776
 
 
777
        this.searchview.on('search_data', self, this.do_searchview_search);
 
778
        return this.searchview.appendTo(this.$el.find(".oe_view_manager_view_search"));
 
779
    },
 
780
    do_searchview_search: function(domains, contexts, groupbys) {
 
781
        var self = this,
 
782
            controller = this.views[this.active_view].controller,
 
783
            action_context = this.action.context || {};
 
784
        instance.web.pyeval.eval_domains_and_contexts({
 
785
            domains: [this.action.domain || []].concat(domains || []),
 
786
            contexts: [action_context].concat(contexts || []),
 
787
            group_by_seq: groupbys || []
 
788
        }).done(function (results) {
 
789
            self.dataset._model = new instance.web.Model(
 
790
                self.dataset.model, results.context, results.domain);
 
791
            var groupby = results.group_by.length
 
792
                        ? results.group_by
 
793
                        : action_context.group_by;
 
794
            if (_.isString(groupby)) {
 
795
                groupby = [groupby];
 
796
            }
 
797
            $.when(controller.do_search(results.domain, results.context, groupby || [])).then(function() {
 
798
                self.view_completely_inited.resolve();
 
799
            });
 
800
        });
 
801
    },
 
802
    /**
 
803
     * Called when one of the view want to execute an action
 
804
     */
 
805
    on_action: function(action) {
 
806
    },
 
807
    on_create: function() {
 
808
    },
 
809
    on_remove: function() {
 
810
    },
 
811
    on_edit: function() {
 
812
    },
 
813
    /**
 
814
     * Called by children view after executing an action
 
815
     */
 
816
    on_action_executed: function () {
 
817
    },
 
818
});
 
819
 
 
820
instance.web.ViewManagerAction = instance.web.ViewManager.extend({
 
821
    template:"ViewManagerAction",
 
822
    /**
 
823
     * @constructs instance.web.ViewManagerAction
 
824
     * @extends instance.web.ViewManager
 
825
     *
 
826
     * @param {instance.web.ActionManager} parent parent object/widget
 
827
     * @param {Object} action descriptor for the action this viewmanager needs to manage its views.
 
828
     */
 
829
    init: function(parent, action) {
 
830
        // dataset initialization will take the session from ``this``, so if we
 
831
        // do not have it yet (and we don't, because we've not called our own
 
832
        // ``_super()``) rpc requests will blow up.
 
833
        var flags = action.flags || {};
 
834
        if (!('auto_search' in flags)) {
 
835
            flags.auto_search = action.auto_search !== false;
 
836
        }
 
837
        if (action.res_model == 'board.board' && action.view_mode === 'form') {
 
838
            // Special case for Dashboards
 
839
            _.extend(flags, {
 
840
                views_switcher : false,
 
841
                display_title : false,
 
842
                search_view : false,
 
843
                pager : false,
 
844
                sidebar : false,
 
845
                action_buttons : false
 
846
            });
 
847
        }
 
848
        this._super(parent, null, action.views, flags);
 
849
        this.session = parent.session;
 
850
        this.action = action;
 
851
        var dataset = new instance.web.DataSetSearch(this, action.res_model, action.context, action.domain);
 
852
        if (action.res_id) {
 
853
            dataset.ids.push(action.res_id);
 
854
            dataset.index = 0;
 
855
        }
 
856
        this.dataset = dataset;
 
857
    },
 
858
    /**
 
859
     * Initializes the ViewManagerAction: sets up the searchview (if the
 
860
     * searchview is enabled in the manager's action flags), calls into the
 
861
     * parent to initialize the primary view and (if the VMA has a searchview)
 
862
     * launches an initial search after both views are done rendering.
 
863
     */
 
864
    start: function() {
 
865
        var self = this,
 
866
            searchview_loaded,
 
867
            search_defaults = {};
 
868
        _.each(this.action.context, function (value, key) {
 
869
            var match = /^search_default_(.*)$/.exec(key);
 
870
            if (match) {
 
871
                search_defaults[match[1]] = value;
 
872
            }
 
873
        });
 
874
        // init search view
 
875
        var searchview_id = this.action['search_view_id'] && this.action['search_view_id'][0];
 
876
 
 
877
        searchview_loaded = this.setup_search_view(searchview_id || false, search_defaults);
 
878
 
 
879
        var main_view_loaded = this._super();
 
880
 
 
881
        var manager_ready = $.when(searchview_loaded, main_view_loaded, this.view_completely_inited);
 
882
 
 
883
        this.$el.find('.oe_debug_view').change(this.on_debug_changed);
 
884
        this.$el.addClass("oe_view_manager_" + (this.action.target || 'current'));
 
885
        return manager_ready;
 
886
    },
 
887
    on_debug_changed: function (evt) {
 
888
        var self = this,
 
889
            $sel = $(evt.currentTarget),
 
890
            $option = $sel.find('option:selected'),
 
891
            val = $sel.val(),
 
892
            current_view = this.views[this.active_view].controller;
 
893
        switch (val) {
 
894
            case 'fvg':
 
895
                var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get"), width: '95%' }).open();
 
896
                $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$el);
 
897
                break;
 
898
            case 'tests':
 
899
                this.do_action({
 
900
                    name: _t("JS Tests"),
 
901
                    target: 'new',
 
902
                    type : 'ir.actions.act_url',
 
903
                    url: '/web/tests?mod=*'
 
904
                });
 
905
                break;
 
906
            case 'perm_read':
 
907
                var ids = current_view.get_selected_ids();
 
908
                if (ids.length === 1) {
 
909
                    this.dataset.call('perm_read', [ids]).done(function(result) {
 
910
                        var dialog = new instance.web.Dialog(this, {
 
911
                            title: _.str.sprintf(_t("View Log (%s)"), self.dataset.model),
 
912
                            width: 400
 
913
                        }, QWeb.render('ViewManagerDebugViewLog', {
 
914
                            perm : result[0],
 
915
                            format : instance.web.format_value
 
916
                        })).open();
 
917
                    });
 
918
                }
 
919
                break;
 
920
            case 'toggle_layout_outline':
 
921
                current_view.rendering_engine.toggle_layout_debugging();
 
922
                break;
 
923
            case 'set_defaults':
 
924
                current_view.open_defaults_dialog();
 
925
                break;
 
926
            case 'translate':
 
927
                this.do_action({
 
928
                    name: _t("Technical Translation"),
 
929
                    res_model : 'ir.translation',
 
930
                    domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']],
 
931
                    views: [[false, 'list'], [false, 'form']],
 
932
                    type : 'ir.actions.act_window',
 
933
                    view_type : "list",
 
934
                    view_mode : "list"
 
935
                });
 
936
                break;
 
937
            case 'fields':
 
938
                this.dataset.call('fields_get', [false, {}]).done(function (fields) {
 
939
                    var $root = $('<dl>');
 
940
                    _(fields).each(function (attributes, name) {
 
941
                        $root.append($('<dt>').append($('<h4>').text(name)));
 
942
                        var $attrs = $('<dl>').appendTo($('<dd>').appendTo($root));
 
943
                        _(attributes).each(function (def, name) {
 
944
                            if (def instanceof Object) {
 
945
                                def = JSON.stringify(def);
 
946
                            }
 
947
                            $attrs
 
948
                                .append($('<dt>').text(name))
 
949
                                .append($('<dd style="white-space: pre-wrap;">').text(def));
 
950
                        });
 
951
                    });
 
952
                    new instance.web.Dialog(self, {
 
953
                        title: _.str.sprintf(_t("Model %s fields"),
 
954
                                             self.dataset.model),
 
955
                        width: '95%'}, $root).open();
 
956
                });
 
957
                break;
 
958
            case 'edit_workflow':
 
959
                return this.do_action({
 
960
                    res_model : 'workflow',
 
961
                    domain : [['osv', '=', this.dataset.model]],
 
962
                    views: [[false, 'list'], [false, 'form'], [false, 'diagram']],
 
963
                    type : 'ir.actions.act_window',
 
964
                    view_type : 'list',
 
965
                    view_mode : 'list'
 
966
                });
 
967
                break;
 
968
            case 'edit':
 
969
                this.do_edit_resource($option.data('model'), $option.data('id'), { name : $option.text() });
 
970
                break;
 
971
            case 'manage_filters':
 
972
                this.do_action({
 
973
                    res_model: 'ir.filters',
 
974
                    views: [[false, 'list'], [false, 'form']],
 
975
                    type: 'ir.actions.act_window',
 
976
                    context: {
 
977
                        search_default_my_filters: true,
 
978
                        search_default_model_id: this.dataset.model
 
979
                    }
 
980
                });
 
981
                break;
 
982
            case 'print_workflow':
 
983
                if (current_view.get_selected_ids  && current_view.get_selected_ids().length == 1) {
 
984
                    instance.web.blockUI();
 
985
                    var action = {
 
986
                        context: { active_ids: current_view.get_selected_ids() },
 
987
                        report_name: "workflow.instance.graph",
 
988
                        datas: {
 
989
                            model: this.dataset.model,
 
990
                            id: current_view.get_selected_ids()[0],
 
991
                            nested: true,
 
992
                        }
 
993
                    };
 
994
                    this.session.get_file({
 
995
                        url: '/web/report',
 
996
                        data: {action: JSON.stringify(action)},
 
997
                        complete: instance.web.unblockUI
 
998
                    });
 
999
                }
 
1000
                break;
 
1001
            default:
 
1002
                if (val) {
 
1003
                    console.log("No debug handler for ", val);
 
1004
                }
 
1005
        }
 
1006
        evt.currentTarget.selectedIndex = 0;
 
1007
    },
 
1008
    do_edit_resource: function(model, id, action) {
 
1009
        var action = _.extend({
 
1010
            res_model : model,
 
1011
            res_id : id,
 
1012
            type : 'ir.actions.act_window',
 
1013
            view_type : 'form',
 
1014
            view_mode : 'form',
 
1015
            views : [[false, 'form']],
 
1016
            target : 'new',
 
1017
            flags : {
 
1018
                action_buttons : true,
 
1019
                form : {
 
1020
                    resize_textareas : true
 
1021
                }
 
1022
            }
 
1023
        }, action || {});
 
1024
        this.do_action(action);
 
1025
    },
 
1026
    switch_mode: function (view_type, no_store, options) {
 
1027
        var self = this;
 
1028
 
 
1029
        return this.alive($.when(this._super.apply(this, arguments))).done(function () {
 
1030
            var controller = self.views[self.active_view].controller;
 
1031
            self.$el.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', {
 
1032
                view: controller,
 
1033
                view_manager: self
 
1034
            }));
 
1035
            self.set_title();
 
1036
        });
 
1037
    },
 
1038
    do_create_view: function(view_type) {
 
1039
        var self = this;
 
1040
        return this._super.apply(this, arguments).then(function() {
 
1041
            var view = self.views[view_type].controller;
 
1042
            view.set({ 'title': self.action.name });
 
1043
        });
 
1044
    },
 
1045
    get_action_manager: function() {
 
1046
        var cur = this;
 
1047
        while (cur = cur.getParent()) {
 
1048
            if (cur instanceof instance.web.ActionManager) {
 
1049
                return cur;
 
1050
            }
 
1051
        }
 
1052
        return undefined;
 
1053
    },
 
1054
    set_title: function(title) {
 
1055
        this.$el.find('.oe_breadcrumb_title:first').html(this.get_action_manager().get_title());
 
1056
    },
 
1057
    do_push_state: function(state) {
 
1058
        if (this.getParent() && this.getParent().do_push_state) {
 
1059
            state["view_type"] = this.active_view;
 
1060
            this.url_states[this.active_view] = state;
 
1061
            this.getParent().do_push_state(state);
 
1062
        }
 
1063
    },
 
1064
    do_load_state: function(state, warm) {
 
1065
        var self = this,
 
1066
            defs = [];
 
1067
        if (state.view_type && state.view_type !== this.active_view) {
 
1068
            defs.push(
 
1069
                this.views[this.active_view].deferred.then(function() {
 
1070
                    return self.switch_mode(state.view_type, true);
 
1071
                })
 
1072
            );
 
1073
        } 
 
1074
 
 
1075
        $.when(defs).done(function() {
 
1076
            self.views[self.active_view].controller.do_load_state(state, warm);
 
1077
        });
 
1078
    },
 
1079
});
 
1080
 
 
1081
instance.web.Sidebar = instance.web.Widget.extend({
 
1082
    init: function(parent) {
 
1083
        var self = this;
 
1084
        this._super(parent);
 
1085
        var view = this.getParent();
 
1086
        this.sections = [
 
1087
            { 'name' : 'print', 'label' : _t('Print'), },
 
1088
            { 'name' : 'other', 'label' : _t('More'), }
 
1089
        ];
 
1090
        this.items = {
 
1091
            'print' : [],
 
1092
            'other' : []
 
1093
        };
 
1094
        this.fileupload_id = _.uniqueId('oe_fileupload');
 
1095
        $(window).on(this.fileupload_id, function() {
 
1096
            var args = [].slice.call(arguments).slice(1);
 
1097
            self.do_attachement_update(self.dataset, self.model_id,args);
 
1098
            instance.web.unblockUI();
 
1099
        });
 
1100
    },
 
1101
    start: function() {
 
1102
        var self = this;
 
1103
        this._super(this);
 
1104
        this.redraw();
 
1105
        this.$el.on('click','.oe_dropdown_menu li a', function(event) {
 
1106
            var section = $(this).data('section');
 
1107
            var index = $(this).data('index');
 
1108
            var item = self.items[section][index];
 
1109
            if (item.callback) {
 
1110
                item.callback.apply(self, [item]);
 
1111
            } else if (item.action) {
 
1112
                self.on_item_action_clicked(item);
 
1113
            } else if (item.url) {
 
1114
                return true;
 
1115
            }
 
1116
            event.preventDefault();
 
1117
        });
 
1118
    },
 
1119
    redraw: function() {
 
1120
        var self = this;
 
1121
        self.$el.html(QWeb.render('Sidebar', {widget: self}));
 
1122
 
 
1123
        // Hides Sidebar sections when item list is empty
 
1124
        this.$('.oe_form_dropdown_section').each(function() {
 
1125
            $(this).toggle(!!$(this).find('li').length);
 
1126
        });
 
1127
 
 
1128
        self.$("[title]").tipsy({
 
1129
            'html': true,
 
1130
            'delayIn': 500,
 
1131
        })
 
1132
    },
 
1133
    /**
 
1134
     * For each item added to the section:
 
1135
     *
 
1136
     * ``label``
 
1137
     *     will be used as the item's name in the sidebar, can be html
 
1138
     *
 
1139
     * ``action``
 
1140
     *     descriptor for the action which will be executed, ``action`` and
 
1141
     *     ``callback`` should be exclusive
 
1142
     *
 
1143
     * ``callback``
 
1144
     *     function to call when the item is clicked in the sidebar, called
 
1145
     *     with the item descriptor as its first argument (so information
 
1146
     *     can be stored as additional keys on the object passed to
 
1147
     *     ``add_items``)
 
1148
     *
 
1149
     * ``classname`` (optional)
 
1150
     *     ``@class`` set on the sidebar serialization of the item
 
1151
     *
 
1152
     * ``title`` (optional)
 
1153
     *     will be set as the item's ``@title`` (tooltip)
 
1154
     *
 
1155
     * @param {String} section_code
 
1156
     * @param {Array<{label, action | callback[, classname][, title]}>} items
 
1157
     */
 
1158
    add_items: function(section_code, items) {
 
1159
        var self = this;
 
1160
        if (items) {
 
1161
            this.items[section_code].push.apply(this.items[section_code],items);
 
1162
            this.redraw();
 
1163
        }
 
1164
    },
 
1165
    add_toolbar: function(toolbar) {
 
1166
        var self = this;
 
1167
        _.each(['print','action','relate'], function(type) {
 
1168
            var items = toolbar[type];
 
1169
            if (items) {
 
1170
                for (var i = 0; i < items.length; i++) {
 
1171
                    items[i] = {
 
1172
                        label: items[i]['name'],
 
1173
                        action: items[i],
 
1174
                        classname: 'oe_sidebar_' + type
 
1175
                    }
 
1176
                }
 
1177
                self.add_items(type=='print' ? 'print' : 'other', items);
 
1178
            }
 
1179
        });
 
1180
    },
 
1181
    on_item_action_clicked: function(item) {
 
1182
        var self = this;
 
1183
        self.getParent().sidebar_eval_context().done(function (sidebar_eval_context) {
 
1184
            var ids = self.getParent().get_selected_ids();
 
1185
            if (ids.length == 0) {
 
1186
                instance.web.dialog($("<div />").text(_t("You must choose at least one record.")), { title: _t("Warning"), modal: true });
 
1187
                return false;
 
1188
            }
 
1189
            var active_ids_context = {
 
1190
                active_id: ids[0],
 
1191
                active_ids: ids,
 
1192
                active_model: self.getParent().dataset.model
 
1193
            }; 
 
1194
            var c = instance.web.pyeval.eval('context',
 
1195
                new instance.web.CompoundContext(
 
1196
                    sidebar_eval_context, active_ids_context));
 
1197
            self.rpc("/web/action/load", {
 
1198
                action_id: item.action.id,
 
1199
                context: c
 
1200
            }).done(function(result) {
 
1201
                result.context = new instance.web.CompoundContext(
 
1202
                    result.context || {}, active_ids_context)
 
1203
                        .set_eval_context(c);
 
1204
                result.flags = result.flags || {};
 
1205
                result.flags.new_window = true;
 
1206
                self.do_action(result, {
 
1207
                    on_close: function() {
 
1208
                        // reload view
 
1209
                        self.getParent().reload();
 
1210
                    },
 
1211
                });
 
1212
            });
 
1213
        });
 
1214
    },
 
1215
    do_attachement_update: function(dataset, model_id, args) {
 
1216
        var self = this;
 
1217
        this.dataset = dataset;
 
1218
        this.model_id = model_id;
 
1219
        if (args && args[0].error) {
 
1220
            this.do_warn( instance.web.qweb.render('message_error_uploading'), args[0].error);
 
1221
        }
 
1222
        if (!model_id) {
 
1223
            this.on_attachments_loaded([]);
 
1224
        } else {
 
1225
            var dom = [ ['res_model', '=', dataset.model], ['res_id', '=', model_id], ['type', 'in', ['binary', 'url']] ];
 
1226
            var ds = new instance.web.DataSetSearch(this, 'ir.attachment', dataset.get_context(), dom);
 
1227
            ds.read_slice(['name', 'url', 'type', 'create_uid', 'create_date', 'write_uid', 'write_date'], {}).done(this.on_attachments_loaded);
 
1228
        }
 
1229
    },
 
1230
    on_attachments_loaded: function(attachments) {
 
1231
        var self = this;
 
1232
        var items = [];
 
1233
        var prefix = this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'});
 
1234
        _.each(attachments,function(a) {
 
1235
            a.label = a.name;
 
1236
            if(a.type === "binary") {
 
1237
                a.url = prefix  + '&id=' + a.id + '&t=' + (new Date().getTime());
 
1238
            }
 
1239
        });
 
1240
        self.items['files'] = attachments;
 
1241
        self.redraw();
 
1242
        this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed);
 
1243
        this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete);
 
1244
    },
 
1245
    on_attachment_changed: function(e) {
 
1246
        var $e = $(e.target);
 
1247
        if ($e.val() !== '') {
 
1248
            this.$el.find('form.oe_form_binary_form').submit();
 
1249
            $e.parent().find('input[type=file]').prop('disabled', true);
 
1250
            $e.parent().find('button').prop('disabled', true).find('img, span').toggle();
 
1251
            this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...'));
 
1252
            instance.web.blockUI();
 
1253
        }
 
1254
    },
 
1255
    on_attachment_delete: function(e) {
 
1256
        e.preventDefault();
 
1257
        e.stopPropagation();
 
1258
        var self = this;
 
1259
        var $e = $(e.currentTarget);
 
1260
        if (confirm(_t("Do you really want to delete this attachment ?"))) {
 
1261
            (new instance.web.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).done(function() {
 
1262
                self.do_attachement_update(self.dataset, self.model_id);
 
1263
            });
 
1264
        }
 
1265
    }
 
1266
});
 
1267
 
 
1268
instance.web.View = instance.web.Widget.extend({
 
1269
    // name displayed in view switchers
 
1270
    display_name: '',
 
1271
    /**
 
1272
     * Define a view type for each view to allow automatic call to fields_view_get.
 
1273
     */
 
1274
    view_type: undefined,
 
1275
    init: function(parent, dataset, view_id, options) {
 
1276
        this._super(parent);
 
1277
        this.ViewManager = parent;
 
1278
        this.dataset = dataset;
 
1279
        this.view_id = view_id;
 
1280
        this.set_default_options(options);
 
1281
    },
 
1282
    start: function () {
 
1283
        return this.load_view();
 
1284
    },
 
1285
    load_view: function(context) {
 
1286
        var self = this;
 
1287
        var view_loaded_def;
 
1288
        if (this.embedded_view) {
 
1289
            view_loaded_def = $.Deferred();
 
1290
            $.async_when().done(function() {
 
1291
                view_loaded_def.resolve(self.embedded_view);
 
1292
            });
 
1293
        } else {
 
1294
            if (! this.view_type)
 
1295
                console.warn("view_type is not defined", this);
 
1296
            view_loaded_def = instance.web.fields_view_get({
 
1297
                "model": this.dataset._model,
 
1298
                "view_id": this.view_id,
 
1299
                "view_type": this.view_type,
 
1300
                "toolbar": !!this.options.$sidebar,
 
1301
                "context": this.dataset.get_context(),
 
1302
            });
 
1303
        }
 
1304
        return view_loaded_def.then(function(r) {
 
1305
            self.fields_view = r;
 
1306
            // add css classes that reflect the (absence of) access rights
 
1307
            self.$el.addClass('oe_view')
 
1308
                .toggleClass('oe_cannot_create', !self.is_action_enabled('create'))
 
1309
                .toggleClass('oe_cannot_edit', !self.is_action_enabled('edit'))
 
1310
                .toggleClass('oe_cannot_delete', !self.is_action_enabled('delete'));
 
1311
            return $.when(self.view_loading(r)).then(function() {
 
1312
                self.trigger('view_loaded', r);
 
1313
            });
 
1314
        });
 
1315
    },
 
1316
    view_loading: function(r) {
 
1317
    },
 
1318
    set_default_options: function(options) {
 
1319
        this.options = options || {};
 
1320
        _.defaults(this.options, {
 
1321
            // All possible views options should be defaulted here
 
1322
            $sidebar: null,
 
1323
            sidebar_id: null,
 
1324
            action: null,
 
1325
            action_views_ids: {}
 
1326
        });
 
1327
    },
 
1328
    /**
 
1329
     * Fetches and executes the action identified by ``action_data``.
 
1330
     *
 
1331
     * @param {Object} action_data the action descriptor data
 
1332
     * @param {String} action_data.name the action name, used to uniquely identify the action to find and execute it
 
1333
     * @param {String} [action_data.special=null] special action handlers (currently: only ``'cancel'``)
 
1334
     * @param {String} [action_data.type='workflow'] the action type, if present, one of ``'object'``, ``'action'`` or ``'workflow'``
 
1335
     * @param {Object} [action_data.context=null] additional action context, to add to the current context
 
1336
     * @param {instance.web.DataSet} dataset a dataset object used to communicate with the server
 
1337
     * @param {Object} [record_id] the identifier of the object on which the action is to be applied
 
1338
     * @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
 
1339
     */
 
1340
    do_execute_action: function (action_data, dataset, record_id, on_closed) {
 
1341
        var self = this;
 
1342
        var result_handler = function () {
 
1343
            if (on_closed) { on_closed.apply(null, arguments); }
 
1344
            if (self.getParent() && self.getParent().on_action_executed) {
 
1345
                return self.getParent().on_action_executed.apply(null, arguments);
 
1346
            }
 
1347
        };
 
1348
        var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {});
 
1349
        var handler = function (action) {
 
1350
            if (action && action.constructor == Object) {
 
1351
                var ncontext = new instance.web.CompoundContext(context);
 
1352
                if (record_id) {
 
1353
                    ncontext.add({
 
1354
                        active_id: record_id,
 
1355
                        active_ids: [record_id],
 
1356
                        active_model: dataset.model
 
1357
                    });
 
1358
                }
 
1359
                ncontext.add(action.context || {});
 
1360
                action.context = ncontext;
 
1361
                return self.do_action(action, {
 
1362
                    on_close: result_handler,
 
1363
                });
 
1364
            } else {
 
1365
                self.do_action({"type":"ir.actions.act_window_close"});
 
1366
                return result_handler();
 
1367
            }
 
1368
        };
 
1369
 
 
1370
        if (action_data.special === 'cancel') {
 
1371
            return handler({"type":"ir.actions.act_window_close"});
 
1372
        } else if (action_data.type=="object") {
 
1373
            var args = [[record_id]], additional_args = [];
 
1374
            if (action_data.args) {
 
1375
                try {
 
1376
                    // Warning: quotes and double quotes problem due to json and xml clash
 
1377
                    // Maybe we should force escaping in xml or do a better parse of the args array
 
1378
                    additional_args = JSON.parse(action_data.args.replace(/'/g, '"'));
 
1379
                    args = args.concat(additional_args);
 
1380
                } catch(e) {
 
1381
                    console.error("Could not JSON.parse arguments", action_data.args);
 
1382
                }
 
1383
            }
 
1384
            args.push(context);
 
1385
            return dataset.call_button(action_data.name, args).then(handler).then(function () {
 
1386
                if (instance.webclient) {
 
1387
                    instance.webclient.menu.do_reload_needaction();
 
1388
                }
 
1389
            });
 
1390
        } else if (action_data.type=="action") {
 
1391
            return this.rpc('/web/action/load', {
 
1392
                action_id: action_data.name,
 
1393
                context: instance.web.pyeval.eval('context', context),
 
1394
                do_not_eval: true
 
1395
            }).then(handler);
 
1396
        } else  {
 
1397
            return dataset.exec_workflow(record_id, action_data.name).then(handler);
 
1398
        }
 
1399
    },
 
1400
    /**
 
1401
     * Directly set a view to use instead of calling fields_view_get. This method must
 
1402
     * be called before start(). When an embedded view is set, underlying implementations
 
1403
     * of instance.web.View must use the provided view instead of any other one.
 
1404
     *
 
1405
     * @param embedded_view A view.
 
1406
     */
 
1407
    set_embedded_view: function(embedded_view) {
 
1408
        this.embedded_view = embedded_view;
 
1409
    },
 
1410
    do_show: function () {
 
1411
        this.$el.show();
 
1412
    },
 
1413
    do_hide: function () {
 
1414
        this.$el.hide();
 
1415
    },
 
1416
    is_active: function () {
 
1417
        var manager = this.getParent();
 
1418
        return !manager || !manager.active_view
 
1419
             || manager.views[manager.active_view].controller === this;
 
1420
    }, /**
 
1421
     * Wraps fn to only call it if the current view is the active one. If the
 
1422
     * current view is not active, doesn't call fn.
 
1423
     *
 
1424
     * fn can not return anything, as a non-call to fn can't return anything
 
1425
     * either
 
1426
     *
 
1427
     * @param {Function} fn function to wrap in the active guard
 
1428
     */
 
1429
    guard_active: function (fn) {
 
1430
        var self = this;
 
1431
        return function () {
 
1432
            if (self.is_active()) {
 
1433
                fn.apply(self, arguments);
 
1434
            }
 
1435
        }
 
1436
    },
 
1437
    do_push_state: function(state) {
 
1438
        if (this.getParent() && this.getParent().do_push_state) {
 
1439
            this.getParent().do_push_state(state);
 
1440
        }
 
1441
    },
 
1442
    do_load_state: function(state, warm) {
 
1443
    },
 
1444
    /**
 
1445
     * Switches to a specific view type
 
1446
     */
 
1447
    do_switch_view: function() { 
 
1448
        this.trigger.apply(this, ['switch_mode'].concat(_.toArray(arguments)));
 
1449
    },
 
1450
    /**
 
1451
     * Cancels the switch to the current view, switches to the previous one
 
1452
     *
 
1453
     * @param {Object} [options]
 
1454
     * @param {Boolean} [options.created=false] resource was created
 
1455
     * @param {String} [options.default=null] view to switch to if no previous view
 
1456
     */
 
1457
 
 
1458
    do_search: function(view) {
 
1459
    },
 
1460
    on_sidebar_export: function() {
 
1461
        new instance.web.DataExport(this, this.dataset).open();
 
1462
    },
 
1463
    sidebar_eval_context: function () {
 
1464
        return $.when({});
 
1465
    },
 
1466
    /**
 
1467
     * Asks the view to reload itself, if the reloading is asynchronous should
 
1468
     * return a {$.Deferred} indicating when the reloading is done.
 
1469
     */
 
1470
    reload: function () {
 
1471
        return $.when();
 
1472
    },
 
1473
    /**
 
1474
     * Return whether the user can perform the action ('create', 'edit', 'delete') in this view.
 
1475
     * An action is disabled by setting the corresponding attribute in the view's main element,
 
1476
     * like: <form string="" create="false" edit="false" delete="false">
 
1477
     */
 
1478
    is_action_enabled: function(action) {
 
1479
        var attrs = this.fields_view.arch.attrs;
 
1480
        return (action in attrs) ? JSON.parse(attrs[action]) : true;
 
1481
    }
 
1482
});
 
1483
 
 
1484
/**
 
1485
 * Performs a fields_view_get and apply postprocessing.
 
1486
 * return a {$.Deferred} resolved with the fvg
 
1487
 *
 
1488
 * @param {Object} args
 
1489
 * @param {String|Object} args.model instance.web.Model instance or string repr of the model
 
1490
 * @param {Object} [args.context] context if args.model is a string
 
1491
 * @param {Number} [args.view_id] id of the view to be loaded, default view if null
 
1492
 * @param {String} [args.view_type] type of view to be loaded if view_id is null
 
1493
 * @param {Boolean} [args.toolbar=false] get the toolbar definition
 
1494
 */
 
1495
instance.web.fields_view_get = function(args) {
 
1496
    function postprocess(fvg) {
 
1497
        var doc = $.parseXML(fvg.arch).documentElement;
 
1498
        fvg.arch = instance.web.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));
 
1499
        if ('id' in fvg.fields) {
 
1500
            // Special case for id's
 
1501
            var id_field = fvg.fields['id'];
 
1502
            id_field.original_type = id_field.type;
 
1503
            id_field.type = 'id';
 
1504
        }
 
1505
        _.each(fvg.fields, function(field) {
 
1506
            _.each(field.views || {}, function(view) {
 
1507
                postprocess(view);
 
1508
            });
 
1509
        });
 
1510
        return fvg;
 
1511
    }
 
1512
    args = _.defaults(args, {
 
1513
        toolbar: false,
 
1514
    });
 
1515
    var model = args.model;
 
1516
    if (typeof model === 'string') {
 
1517
        model = new instance.web.Model(args.model, args.context);
 
1518
    }
 
1519
    return args.model.call('fields_view_get', [args.view_id, args.view_type, args.context, args.toolbar]).then(function(fvg) {
 
1520
        return postprocess(fvg);
 
1521
    });
 
1522
};
 
1523
 
 
1524
instance.web.xml_to_json = function(node, strip_whitespace) {
 
1525
    switch (node.nodeType) {
 
1526
        case 9:
 
1527
            return instance.web.xml_to_json(node.documentElement, strip_whitespace);
 
1528
        case 3:
 
1529
        case 4:
 
1530
            return (strip_whitespace && node.data.trim() === '') ? undefined : node.data;
 
1531
        case 1:
 
1532
            var attrs = $(node).getAttributes();
 
1533
            _.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) {
 
1534
                if (attrs[key]) {
 
1535
                    try {
 
1536
                        attrs[key] = JSON.parse(attrs[key]);
 
1537
                    } catch(e) { }
 
1538
                }
 
1539
            });
 
1540
            return {
 
1541
                tag: node.tagName.toLowerCase(),
 
1542
                attrs: attrs,
 
1543
                children: _.compact(_.map(node.childNodes, function(node) {
 
1544
                    return instance.web.xml_to_json(node, strip_whitespace);
 
1545
                })),
 
1546
            }
 
1547
    }
 
1548
}
 
1549
instance.web.json_node_to_xml = function(node, human_readable, indent) {
 
1550
    // For debugging purpose, this function will convert a json node back to xml
 
1551
    indent = indent || 0;
 
1552
    var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''),
 
1553
        r = sindent + '<' + node.tag,
 
1554
        cr = human_readable ? '\n' : '';
 
1555
 
 
1556
    if (typeof(node) === 'string') {
 
1557
        return sindent + node;
 
1558
    } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) {
 
1559
        throw new Error(
 
1560
            _.str.sprintf(_t("Node [%s] is not a JSONified XML node"),
 
1561
                          JSON.stringify(node)));
 
1562
    }
 
1563
    for (var attr in node.attrs) {
 
1564
        var vattr = node.attrs[attr];
 
1565
        if (typeof(vattr) !== 'string') {
 
1566
            // domains, ...
 
1567
            vattr = JSON.stringify(vattr);
 
1568
        }
 
1569
        vattr = vattr.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
 
1570
        if (human_readable) {
 
1571
            vattr = vattr.replace(/&quot;/g, "'");
 
1572
        }
 
1573
        r += ' ' + attr + '="' + vattr + '"';
 
1574
    }
 
1575
    if (node.children && node.children.length) {
 
1576
        r += '>' + cr;
 
1577
        var childs = [];
 
1578
        for (var i = 0, ii = node.children.length; i < ii; i++) {
 
1579
            childs.push(instance.web.json_node_to_xml(node.children[i], human_readable, indent + 1));
 
1580
        }
 
1581
        r += childs.join(cr);
 
1582
        r += cr + sindent + '</' + node.tag + '>';
 
1583
        return r;
 
1584
    } else {
 
1585
        return r + '/>';
 
1586
    }
 
1587
};
 
1588
instance.web.xml_to_str = function(node) {
 
1589
    var str = "";
 
1590
    if (window.XMLSerializer) {
 
1591
        str = (new XMLSerializer()).serializeToString(node);
 
1592
    } else if (window.ActiveXObject) {
 
1593
        str = node.xml;
 
1594
    } else {
 
1595
        throw new Error(_t("Could not serialize XML"));
 
1596
    }
 
1597
    // Browsers won't deal with self closing tags except void elements:
 
1598
    // http://www.w3.org/TR/html-markup/syntax.html
 
1599
    var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' ');
 
1600
 
 
1601
    // The following regex is a bit naive but it's ok for the xmlserializer output
 
1602
    str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) {
 
1603
        if (void_elements.indexOf(tag) < 0) {
 
1604
            return "<" + tag + attrs + "></" + tag + ">";
 
1605
        } else {
 
1606
            return match;
 
1607
        }
 
1608
    });
 
1609
    return str;
 
1610
};
 
1611
 
 
1612
/**
 
1613
 * Registry for all the main views
 
1614
 */
 
1615
instance.web.views = new instance.web.Registry();
 
1616
 
 
1617
};
 
1618
 
 
1619
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: