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