Example #1
0
odoo.define('hr_attendance.my_attendances', function (require) {
"use strict";

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');


var MyAttendances = AbstractAction.extend({
    contentTemplate: 'HrAttendanceMyMainMenu',
    events: {
        "click .o_hr_attendance_sign_in_out_icon": _.debounce(function() {
            this.update_attendance();
        }, 200, true),
    },

    willStart: function () {
        var self = this;

        var def = this._rpc({
                model: 'hr.employee',
                method: 'search_read',
                args: [[['user_id', '=', this.getSession().uid]], ['attendance_state', 'name']],
            })
            .then(function (res) {
                self.employee = res.length && res[0];
            });

        return $.when(def, this._super.apply(this, arguments));
    },

    update_attendance: function () {
        var self = this;
        this._rpc({
                model: 'hr.employee',
                method: 'attendance_manual',
                args: [[self.employee.id], 'hr_attendance.hr_attendance_action_my_attendances'],
            })
            .then(function(result) {
                if (result.action) {
                    self.do_action(result.action);
                } else if (result.warning) {
                    self.do_warn(result.warning);
                }
            });
    },
});

core.action_registry.add('hr_attendance_my_attendances', MyAttendances);

return MyAttendances;

});
odoo.define('stock.stock_report_generic', function (require) {
'use strict';

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var ControlPanelMixin = require('web.ControlPanelMixin');
var session = require('web.session');
var ReportWidget = require('stock.ReportWidget');
var framework = require('web.framework');
var crash_manager = require('web.crash_manager');

var QWeb = core.qweb;

var stock_report_generic = AbstractAction.extend(ControlPanelMixin, {
    // Stores all the parameters of the action.
    init: function(parent, action) {
        this.actionManager = parent;
        this.given_context = {};
        this.controller_url = action.context.url;
        if (action.context.context) {
            this.given_context = action.context.context;
        }
        this.given_context.active_id = action.context.active_id || action.params.active_id;
        this.given_context.model = action.context.active_model || false;
        this.given_context.ttype = action.context.ttype || false;
        this.given_context.auto_unfold = action.context.auto_unfold || false;
        this.given_context.lot_name = action.context.lot_name || false;
        return this._super.apply(this, arguments);
    },
    willStart: function() {
        return this.get_html();
    },
    set_html: function() {
        var self = this;
        var def = $.when();
        if (!this.report_widget) {
            this.report_widget = new ReportWidget(this, this.given_context);
            def = this.report_widget.appendTo(this.$el);
        }
        def.then(function () {
            self.report_widget.$el.html(self.html);
            self.report_widget.$el.find('.o_report_heading').html('<h1>Traceability Report</h1>')
            if (self.given_context.auto_unfold) {
                _.each(self.$el.find('.fa-caret-right'), function (line) {
                    self.report_widget.autounfold(line, self.given_context.lot_name);
                });
            }
        });
    },
    start: function() {
        this.set_html();
        return this._super();
    },
    // Fetches the html and is previous report.context if any, else create it
    get_html: function() {
        var self = this;
        var defs = [];
        return this._rpc({
                model: 'stock.traceability.report',
                method: 'get_html',
                args: [self.given_context],
            })
            .then(function (result) {
                self.html = result.html;
                self.renderButtons();
                defs.push(self.update_cp());
                return $.when.apply($, defs);
            });
    },
    // Updates the control panel and render the elements that have yet to be rendered
    update_cp: function() {
        if (!this.$buttons) {
            this.renderButtons();
        }
        var status = {
            cp_content: {$buttons: this.$buttons},
        };
        return this.update_control_panel(status);
    },
    renderButtons: function() {
        var self = this;
        this.$buttons = $(QWeb.render("stockReports.buttons", {}));
        // pdf output
        this.$buttons.bind('click', function () {
            var $element = $(self.$el[0]).find('.o_stock_reports_table tbody tr');
            var dict = [];

            $element.each(function( index ) {
                var $el = $($element[index]);
                dict.push({
                        'id': $el.data('id'),
                        'model_id': $el.data('model_id'),
                        'model_name': $el.data('model'),
                        'unfoldable': $el.data('unfold'),
                        'level': $el.find('td:first').data('level') || 1
                });
            });
            framework.blockUI();
            var url_data = self.controller_url.replace('active_id', self.given_context['active_id']);
            session.get_file({
                url: url_data.replace('output_format', 'pdf'),
                data: {data: JSON.stringify(dict)},
                complete: framework.unblockUI,
                error: crash_manager.rpc_error.bind(crash_manager),
            });
        });
        return this.$buttons;
    },
    do_show: function() {
        this._super();
        this.update_cp();
    },
});

core.action_registry.add("stock_report_generic", stock_report_generic);
return stock_report_generic;
});
Example #3
0
odoo.define('web.ChangePassword', function (require) {
"use strict";

/**
 * This file defines a client action that opens in a dialog (target='new') and
 * allows the user to change his password.
 */

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var Dialog = require('web.Dialog');
var web_client = require('web.web_client');

var _t = core._t;

var ChangePassword = AbstractAction.extend({
    template: "ChangePassword",

    /**
     * @fixme: weird interaction with the parent for the $buttons handling
     *
     * @override
     * @returns {Deferred}
     */
    start: function () {
        var self = this;
        web_client.set_title(_t("Change Password"));
        var $button = self.$('.oe_form_button');
        $button.appendTo(this.getParent().$footer);
        $button.eq(1).click(function () {
            self.$el.parents('.modal').modal('hide');
        });
        $button.eq(0).click(function () {
            self._rpc({
                    route: '/web/session/change_password',
                    params: {
                        fields: $('form[name=change_password_form]').serializeArray()
                    }
                })
                .done(function (result) {
                    if (result.error) {
                        self._display_error(result);
                    } else {
                    self.do_action('logout');
                    }
                });
        });
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Displays the error in a dialog
     *
     * @private
     * @param {Object} error
     * @param {string} error.error
     * @param {string} error.title
     */
    _display_error: function (error) {
        return new Dialog(this, {
            size: 'medium',
            title: error.title,
            $content: $('<div>').html(error.error)
        }).open();
    },
});

core.action_registry.add("change_password", ChangePassword);

return ChangePassword;

});
Example #4
0
odoo.define('sale_timesheet.ProjectPlan', function (require) {
'use strict';

var AbstractAction = require('web.AbstractAction');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var data = require('web.data');
var pyUtils = require('web.py_utils');
var SearchView = require('web.SearchView');

var _t = core._t;
var QWeb = core.qweb;

var ProjectPlan = AbstractAction.extend(ControlPanelMixin, {
    events: {
        "click a[type='action']": "_onClickAction",
        "click .o_timesheet_plan_redirect": '_onRedirect',
        "click .oe_stat_button": "_onClickStatButton",
        "click .o_timesheet_plan_non_billable_task": "_onClickNonBillableTask",
        "click .o_timesheet_plan_sale_timesheet_people_time .progress-bar": '_onClickEmployeeProgressbar',
    },
    /**
     * @override
     */
     init: function (parent, action) {
        this._super.apply(this, arguments);
        this.action = action;
        this.action_manager = parent;
        this.set('title', action.name || _t('Overview'));
        this.project_ids = [];
    },
    /**
     * @override
     */
    willStart: function () {
        var self = this;
        var view_id = this.action && this.action.search_view_id && this.action.search_view_id[0];
        var def = this
            .loadViews('project.project', this.action.context || {}, [[view_id, 'search']])
            .then(function (result) {
                self.fields_view = result.search;
            });
        return $.when(this._super(), def);
    },
    /**
     * @override
     */
    start: function () {
        var self = this;

        // find default search from context
        var search_defaults = {};
        var context = this.action.context || [];
        _.each(context, function (value, key) {
            var match = /^search_default_(.*)$/.exec(key);
            if (match) {
                search_defaults[match[1]] = value;
            }
        });

        // create searchview
        var options = {
            $buttons: $("<div>"),
            action: this.action,
            disable_groupby: true,
            search_defaults: search_defaults,
        };

        var dataset = new data.DataSetSearch(this, 'project.project');
        this.searchview = new SearchView(this, dataset, this.fields_view, options);
        this.searchview.on('search', this, this._onSearch);

        var def1 = this._super.apply(this, arguments);
        var def2 = this.searchview.appendTo($("<div>")).then(function () {
            self.$searchview_buttons = self.searchview.$buttons.contents();
        });

        return $.when(def1, def2).then(function(){
            self.searchview.do_search();
        });
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    do_show: function () {
        this._super.apply(this, arguments);
        this.searchview.do_search();
        this.action_manager.do_push_state({
            action: this.action.id,
            active_id: this.action.context.active_id,
        });
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Refresh the DOM html
     *
     * @private
     * @param {string|html} dom
     */
    _refreshPlan: function (dom) {
        this.$el.html(dom);
    },

    /**
     * Call controller to get the html content
     *
     * @private
     * @param {string[]}
     * @returns {Deferred}
     */
    _fetchPlan: function (domain) {
        var self = this;
        return this._rpc({
            route:"/timesheet/plan",
            params: {domain: domain},
        }).then(function(result){
            self._refreshPlan(result.html_content);
            self._updateControlPanel(result.actions);
            self.project_ids = result.project_ids;
        });
    },
    /**
     * @private
     */
    _updateControlPanel: function (buttons) {
        // set actions button
        if (this.$buttons) {
            this.$buttons.off().destroy();
        }
        var buttons = buttons || [];
        this.$buttons = $(QWeb.render("project.plan.ControlButtons", {'buttons': buttons}));
        this.$buttons.on('click', '.o_timesheet_plan_btn_action', this._onClickControlButton.bind(this));
        // refresh control panel
        this.update_control_panel({
            cp_content: {
                $buttons: this.$buttons,
                $searchview: this.searchview.$el,
                $searchview_buttons: this.$searchview_buttons,
            },
            searchview: this.searchview,
        });
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * Generate the action to execute based on the clicked target
     *
     * @private
     * @param {MouseEvent} event
     */
    _onClickAction: function (ev) {
        var $target = this.$(ev.currentTarget);

        var action = false;
        if($target.attr('name')){ // classical case : name="action_id" type="action"
            action = $target.attr('name');
        } else { // custom case : build custom action dict
            action = {
                'name': _t('Timesheet'),
                'type': 'ir.actions.act_window',
                'target': 'current',
                'res_model': 'account.analytic.line',
            };
            // find action views
            var views = [[false, 'pivot'], [false, 'list']];
            if($target.attr('views')){
                views = JSON.parse($target.attr('views').replace(/\'/g, '"'));
            }
            action.views = views;
            action.view_mode = _.map(views, function(view_array){return view_array[1];});
            // custom domain
            var domain = [];
            if($target.attr('domain')){
                domain = JSON.parse($target.attr('domain').replace(/\'/g, '"'));
            }
            action.domain = domain;
        }

        // additionnal context
        var context = {
            active_id: this.action.context.active_id,
            active_ids: this.action.context.active_ids,
            active_model: this.action.context.active_model,
        };

        if($target.attr('context')){
            var ctx_str = $target.attr('context').replace(/\'/g, '"');
            context = _.extend(context, JSON.parse(ctx_str));
        }

        this.do_action(action, {
            additional_context: context
        });
    },
    /**
     * Call the action of the action button from control panel, based on the data attribute on the button DOM
     *
     * @param {MouseEvent} event
     * @private
     */
    _onClickControlButton: function (ev) {
        var $target = $(ev.target);
        var action_id = $target.data('action-id');
        var context = $target.data('context');

        return this.do_action(action_id, {
            'additional_context': context,
        });
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onClickEmployeeProgressbar: function (event) {
        var domain = $(event.currentTarget).data('domain');
        this.do_action({
            name: 'Timesheets',
            type: 'ir.actions.act_window',
            res_model: 'account.analytic.line',
            views: [[false, 'list'], [false, 'form']],
            view_type: 'list',
            view_mode: 'form',
            domain: domain || [],
        });
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onClickStatButton: function (event) {
        var self = this;
        var data = $(event.currentTarget).data();
        var parameters = {
            domain: data.domain || [],
            res_model: data.resModel,
        }
        if (data.resId) {
            parameters['res_id'] = data.resId;
        }
        return this._rpc({
            route:"/timesheet/plan/action",
            params: parameters,
        }).then(function(action){
            self.do_action(action);
        });
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onClickNonBillableTask: function (event) {
        var self = this;
        this.do_action({
            name: _t('Non Billable Tasks'),
            type: 'ir.actions.act_window',
            view_type: 'form',
            view_mode: 'form',
            res_model: 'project.task',
            views: [[false, 'list'], [false, 'form']],
            domain: [['project_id', 'in', this.project_ids || []], ['sale_line_id', '=', false]]
        });
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onRedirect: function (event) {
        event.preventDefault();
        var $target = $(event.target);
        this.do_action({
            type: 'ir.actions.act_window',
            view_type: 'form',
            view_mode: 'form',
            res_model: $target.data('oe-model'),
            views: [[false, 'form']],
            res_id: $target.data('oe-id'),
        });
    },
    /**
     * This client action has its own search view and already listen on it. If
     * we let the event pass through, it will be caught by the action manager
     * which will do its work.  This may crash the web client, if the action
     * manager tries to notify the previous action.
     *
     * @private
     * @param {OdooEvent} event
     */
    _onSearch: function (event) {
        event.stopPropagation();
        var session = this.getSession();
        // group by are disabled, so we don't take care of them
        var result = pyUtils.eval_domains_and_contexts({
            domains: event.data.domains,
            contexts: [session.user_context].concat(event.data.contexts)
        });

        this._fetchPlan(result.domain);
    },
});

core.action_registry.add('timesheet.plan', ProjectPlan);

return ProjectPlan;
});
Example #5
0
odoo.define('sale_timesheet.ProjectPlan', function (require) {
'use strict';

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');

var _t = core._t;
var QWeb = core.qweb;

var ProjectPlan = AbstractAction.extend({
    custom_events: {
        search: '_onSearch',
    },
    events: {
        "click a[type='action']": "_onClickAction",
        "click .o_timesheet_plan_redirect": '_onRedirect',
        "click .oe_stat_button": "_onClickStatButton",
        "click .o_timesheet_plan_non_billable_task": "_onClickNonBillableTask",
        "click .o_timesheet_plan_sale_timesheet_people_time .progress-bar": '_onClickEmployeeProgressbar',
    },
    hasControlPanel: true,
    loadControlPanel: true,
    withSearchBar: true,
    searchMenuTypes: ['filter', 'favorite'],
    /**
     * @override
     */
    init: function (parent, action) {
        this._super.apply(this, arguments);
        this.action = action;
        this.context = action.context;
        this.action_manager = parent;
        this._title = action.name || _t('Overview');
        this.project_ids = [];

        this.controlPanelParams.modelName = 'project.project';
    },
    /**
     * @override
     */
    start: function () {
        var self = this;
        return this._super.apply(this, arguments).then(function () {
            // TODO: maybe not the correct way
            return self._controlPanel._reportNewQueryAndRender();
        });
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    do_show: function () {
        this._super.apply(this, arguments);
        this._updateControlPanel();
        this.action_manager.do_push_state({
            action: this.action.id,
            active_id: this.action.context.active_id,
        });
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Refresh the DOM html
     *
     * @private
     * @param {string|html} dom
     */
    _refreshPlan: function (dom) {
        this.$('.o_content').html(dom);
    },

    /**
     * Call controller to get the html content
     *
     * @private
     * @param {string[]}
     * @returns {Deferred}
     */
    _fetchPlan: function (domain) {
        var self = this;
        return this._rpc({
            route:"/timesheet/plan",
            params: {domain: domain},
        }).then(function(result){
            self._refreshPlan(result.html_content);
            self._updateControlPanel(result.actions);
            self.project_ids = result.project_ids;
        });
    },
    /**
     * @private
     */
    _updateControlPanel: function (buttons) {
        // set actions button
        if (this.$buttons) {
            this.$buttons.off().remove();
        }
        buttons = buttons || [];
        this.$buttons = $(QWeb.render("project.plan.ControlButtons", {'buttons': buttons}));
        this.$buttons.on('click', '.o_timesheet_plan_btn_action', this._onClickControlButton.bind(this));
        // refresh control panel
        this.updateControlPanel({
            cp_content: {
                $buttons: this.$buttons,
            },
        });
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * Generate the action to execute based on the clicked target
     *
     * @private
     * @param {MouseEvent} event
     */
    _onClickAction: function (ev) {
        var $target = this.$(ev.currentTarget);

        var action = false;
        if($target.attr('name')){ // classical case : name="action_id" type="action"
            action = $target.attr('name');
        } else { // custom case : build custom action dict
            action = {
                'name': _t('Timesheet'),
                'type': 'ir.actions.act_window',
                'target': 'current',
                'res_model': 'account.analytic.line',
            };
            // find action views
            var views = [[false, 'pivot'], [false, 'list']];
            if($target.attr('views')){
                views = JSON.parse($target.attr('views').replace(/\'/g, '"'));
            }
            action.views = views;
            action.view_mode = _.map(views, function(view_array){return view_array[1];});
            // custom domain
            var domain = [];
            if($target.attr('domain')){
                domain = JSON.parse($target.attr('domain').replace(/\'/g, '"'));
            }
            action.domain = domain;
        }

        // additionnal context
        var context = {
            active_id: this.action.context.active_id,
            active_ids: this.action.context.active_ids,
            active_model: this.action.context.active_model,
        };

        if($target.attr('context')){
            var ctx_str = $target.attr('context').replace(/\'/g, '"');
            context = _.extend(context, JSON.parse(ctx_str));
        }

        this.do_action(action, {
            additional_context: context
        });
    },
    /**
     * Call the action of the action button from control panel, based on the data attribute on the button DOM
     *
     * @param {MouseEvent} event
     * @private
     */
    _onClickControlButton: function (ev) {
        var $target = $(ev.target);
        var action_id = $target.data('action-id');
        var context = $target.data('context');

        return this.do_action(action_id, {
            'additional_context': context,
        });
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onClickEmployeeProgressbar: function (event) {
        var domain = $(event.currentTarget).data('domain');
        this.do_action({
            name: 'Timesheets',
            type: 'ir.actions.act_window',
            res_model: 'account.analytic.line',
            views: [[false, 'list'], [false, 'form']],
            view_type: 'list',
            view_mode: 'form',
            domain: domain || [],
        });
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onClickStatButton: function (event) {
        var self = this;
        var data = $(event.currentTarget).data();
        var parameters = {
            domain: data.domain || [],
            res_model: data.resModel,
        };
        if (data.resId) {
            parameters.res_id = data.resId;
        }
        return this._rpc({
            route:"/timesheet/plan/action",
            params: parameters,
        }).then(function(action){
            self.do_action(action);
        });
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onClickNonBillableTask: function () {
        this.do_action({
            name: _t('Non Billable Tasks'),
            type: 'ir.actions.act_window',
            view_type: 'form',
            view_mode: 'form',
            res_model: 'project.task',
            views: [[false, 'list'], [false, 'form']],
            domain: [['project_id', 'in', this.project_ids || []], ['sale_line_id', '=', false]]
        });
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onRedirect: function (event) {
        event.preventDefault();
        var $target = $(event.target);
        this.do_action({
            type: 'ir.actions.act_window',
            view_type: 'form',
            view_mode: 'form',
            res_model: $target.data('oe-model'),
            views: [[false, 'form']],
            res_id: $target.data('oe-id'),
        });
    },
    /**
     * This client action has its own search view and already listen on it. If
     * we let the event pass through, it will be caught by the action manager
     * which will do its work.  This may crash the web client, if the action
     * manager tries to notify the previous action.
     *
     * @private
     * @param {OdooEvent} event
     */
    _onSearch: function (event) {
        event.stopPropagation();
        this._fetchPlan(event.data.domain);
    },
});

core.action_registry.add('timesheet.plan', ProjectPlan);

return ProjectPlan;
});
Example #6
0
odoo.define('report.client_action', function (require) {
'use strict';

var AbstractAction = require('web.AbstractAction');
var config = require('web.config');
var core = require('web.core');
var ControlPanelMixin = require('web.ControlPanelMixin');
var session = require('web.session');
var utils = require('report.utils');

var QWeb = core.qweb;


var AUTHORIZED_MESSAGES = [
    'report.editor:save_ok',
    'report.editor:discard_ok',
    'report:do_action',
];

var ReportAction = AbstractAction.extend(ControlPanelMixin, {

    template: 'report.client_action',

    init: function (parent, action, options) {
        this._super.apply(this, arguments);

        options = options || {};

        this.action_manager = parent;
        this.title = options.display_name || options.name;

        this.edit_mode_available = false;
        this.in_edit_mode = false;
        this.report_url = options.report_url;

        // Extra info that will be useful to build a qweb-pdf action.
        this.report_name = options.report_name;
        this.report_file = options.report_file;
        this.data = options.data || {};
        this.context = options.context || {};
    },

    start: function () {
        var self = this;
        this.set('title', this.title);
        this.iframe = this.$('iframe')[0];
        return $.when(this._super.apply(this, arguments), session.is_bound).then(function () {
            var web_base_url = session['web.base.url'];
            var trusted_host = utils.get_host_from_url(web_base_url);
            var trusted_protocol = utils.get_protocol_from_url(web_base_url);
            self.trusted_origin = utils.build_origin(trusted_protocol, trusted_host);

            self.$buttons = $(QWeb.render('report.client_action.ControlButtons', {}));
            self.$buttons.on('click', '.o_report_edit', self.on_click_edit);
            self.$buttons.on('click', '.o_report_print', self.on_click_print);
            self.$buttons.on('click', '.o_report_save', self.on_click_save);
            self.$buttons.on('click', '.o_report_discard', self.on_click_discard);

            self._update_control_panel();

            // Load the report in the iframe. Note that we use a relative URL.
            self.iframe.src = self.report_url;

            // Once the iframe is loaded, check if we can edit the report.
            self.iframe.onload = function () {
                self._on_iframe_loaded();
            };
        });
    },

    do_show: function () {
        this._update_control_panel();
        return this._super.apply(this, arguments);
    },

    on_attach_callback: function () {
        // Register now the postMessage event handler. We only want to listen to ~trusted
        // messages and we can only filter them by their origin, so we chose to ignore the
        // messages that do not come from `web.base.url`.
        $(window).on('message', this, this.on_message_received);
    },

    on_detach_callback: function () {
        $(window).off('message', this.on_message_received);
    },

    _on_iframe_loaded: function () {
        var editable = $(this.iframe).contents().find('html').data('editable');
        if (editable === 1) {
            this.edit_mode_available = true;
            this._update_control_panel();
        }
    },

    _update_control_panel: function () {
        this.update_control_panel({
            cp_content: {
                $buttons: this.$buttons,
            },
        });
        this._update_control_panel_buttons();
    },

    /**
     * Helper allowing to toggle groups of buttons in the control panel
     * according to the `this.in_edit_mode` flag.
     */
    _update_control_panel_buttons: function () {
        this.$buttons.filter('div.o_report_edit_mode').toggle(this.in_edit_mode);
        this.$buttons.filter('div.o_report_no_edit_mode').toggle(! this.in_edit_mode);
        this.$buttons.filter('div.o_edit_mode_available').toggle(config.debug && this.edit_mode_available && ! this.in_edit_mode);
    },

    /**
     * Event handler of the message post. We only handle them if they're from
     * `web.base.url` host and protocol and if they're part of `AUTHORIZED_MESSAGES`.
     */
    on_message_received: function (ev) {
        // Check the origin of the received message.
        var message_origin = utils.build_origin(ev.originalEvent.source.location.protocol, ev.originalEvent.source.location.host);
        if (message_origin === this.trusted_origin) {

            // Check the syntax of the received message.
            var message = ev.originalEvent.data;
            if (_.isObject(message)) {
                message = message.message;
            }
            if (! _.isString(message) || (_.isString(message) && ! _.contains(AUTHORIZED_MESSAGES, message))) {
                return;
            }

            switch(message) {
                case 'report.editor:save_ok':
                    // Reload the iframe in order to disable the editor.
                    this.iframe.src = this.report_url;
                    this.in_edit_mode = false;
                    this._update_control_panel_buttons();
                    break;
                case 'report.editor:discard_ok':
                    // Reload the iframe in order to disable the editor.
                    this.iframe.src = this.report_url;
                    this.in_edit_mode = false;
                    this._update_control_panel_buttons();
                    break;
                case 'report:do_action':
                    return this.do_action(ev.originalEvent.data.action);
                default:
            }
        }
    },

    /**
     * Helper allowing to send a message to the `this.el` iframe's window and
     * seting the `targetOrigin` as `this.trusted_origin` (which is the 
     * `web.base.url` ir.config_parameter key) - in other word, only when using
     * this method we only send the message to a trusted domain.
     */
    _post_message: function (message) {
        this.iframe.contentWindow.postMessage(message, this.trusted_origin);
    },

    on_click_edit: function () {
        // We reload the iframe with a special query string to enable the editor.
        if (this.report_url.indexOf('?') === -1) {
            this.iframe.src = this.report_url + '?enable_editor=1';
        } else {
            this.iframe.src = this.report_url + '&enable_editor=1';
        }
        this.in_edit_mode = true;
        this._update_control_panel_buttons();
    },

    on_click_discard: function () {
        this._post_message('report.editor:ask_discard');
    },

    on_click_save: function () {
        this._post_message('report.editor:ask_save');
    },

    on_click_print: function () {
        var action = {
            'type': 'ir.actions.report',
            'report_type': 'qweb-pdf',
            'report_name': this.report_name,
            'report_file': this.report_file,
            'data': this.data,
            'context': this.context,
            'display_name': this.title,
        };
        return this.do_action(action);
    },

});

core.action_registry.add('report.client_action', ReportAction);

return ReportAction;

});
odoo.define('web.AbstractController', function (require) {
"use strict";

/**
 * The Controller class is the class coordinating the model and the renderer.
 * It is the C in MVC, and is what was formerly known in Odoo as a View.
 *
 * Its role is to listen to events bubbling up from the model/renderer, and call
 * the appropriate methods if necessary.  It also render control panel buttons,
 * and react to changes in the search view.  Basically, all interactions from
 * the renderer/model with the outside world (meaning server/reading in session/
 * reading localstorage, ...) has to go through the controller.
 */

var concurrency = require('web.concurrency');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var AbstractAction = require('web.AbstractAction');

var QWeb = core.qweb;

var AbstractController = AbstractAction.extend(ControlPanelMixin, {
    custom_events: {
        open_record: '_onOpenRecord',
        switch_view: '_onSwitchView',
    },
    events: {
        'click a[type="action"]': '_onActionClicked',
    },

    /**
     * @constructor
     * @param {Widget} parent
     * @param {AbstractModel} model
     * @param {AbstractRenderer} renderer
     * @param {object} params
     * @param {string} params.modelName
     * @param {string} [params.controllerID] an id to ease the communication
     *   with upstream components
     * @param {any} [params.handle] a handle that will be given to the model (some id)
     * @param {any} params.initialState the initialState
     * @param {boolean} params.isMultiRecord
     * @param {Object[]} params.actionViews
     * @param {string} params.viewType
     * @param {boolean} params.withControlPanel set to false to hide the
     *   ControlPanel
     */
    init: function (parent, model, renderer, params) {
        this._super.apply(this, arguments);
        this.model = model;
        this.renderer = renderer;
        this.modelName = params.modelName;
        this.handle = params.handle;
        this.activeActions = params.activeActions;
        this.controllerID = params.controllerID;
        this.initialState = params.initialState;

        // use a DropPrevious to correctly handle concurrent updates
        this.dp = new concurrency.DropPrevious();
        // those arguments are temporary, they won't be necessary as soon as the
        // ControlPanel will be handled by the View
        this.displayName = params.displayName;
        this.isMultiRecord = params.isMultiRecord;
        this.searchable = params.searchable;
        this.searchView = params.searchView;
        this.searchViewHidden = params.searchViewHidden;
        this.groupable = params.groupable;
        this.actionViews = params.actionViews;
        this.viewType = params.viewType;
        this.withControlPanel = params.withControlPanel !== false;
        // override this.need_control_panel so that the ActionManager doesn't
        // update the control panel when it isn't visible (this is a temporary
        // hack that can be removed as soon as the CP'll be handled by the view)
        this.need_control_panel = this.withControlPanel;
    },
    /**
     * Simply renders and updates the url.
     *
     * @returns {Deferred}
     */
    start: function () {
        var self = this;

        this.$el.addClass('o_view_controller');

        // render the ControlPanel elements (buttons, pager, sidebar...)
        this.controlPanelElements = this._renderControlPanelElements();

        return $.when(
            this._super.apply(this, arguments),
            this.renderer.appendTo(this.$el)
        ).then(function () {
            self._update(self.initialState);
        });
    },
    /**
     * @override
     */
    destroy: function () {
        if (this.$buttons) {
            this.$buttons.off();
        }
        if (this.controlPanelElements && this.controlPanelElements.$switch_buttons) {
            this.controlPanelElements.$switch_buttons.off();
        }
        return this._super.apply(this, arguments);
    },
    /**
     * Called each time the controller is attached into the DOM.
     */
    on_attach_callback: function () {
        if (this.searchView) {
            this.searchView.on_attach_callback();
        }
        this.renderer.on_attach_callback();
    },
    /**
     * Called each time the controller is detached from the DOM.
     */
    on_detach_callback: function () {
        this.renderer.on_detach_callback();
    },

    /**
     * Gives the focus to the renderer
     */
    giveFocus:function(){
        this.renderer.giveFocus();
    },
    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    canBeRemoved: function () {
        // AAB: get rid of this option when on_hashchange mechanism is improved
        return this.discardChanges(undefined, {readonlyIfRealDiscard: true});
    },
    /**
     * Discards the changes made on the record associated to the given ID, or
     * all changes made by the current controller if no recordID is given. For
     * example, when the user opens the 'home' screen, the action manager calls
     * this method on the active view to make sure it is ok to open the home
     * screen (and lose all current state).
     *
     * Note that it returns a deferred, because the view could choose to ask the
     * user if he agrees to discard.
     *
     * @param {string} [recordID]
     *        if not given, we consider all the changes made by the controller
     * @returns {Deferred} resolved if properly discarded, rejected otherwise
     */
    discardChanges: function (recordID) {
        return $.when();
    },
    /**
     * Returns any special keys that may be useful when reloading the view to
     * get the same effect.  This is necessary for saving the current view in
     * the favorites.  For example, a graph view might want to add a key to
     * save the current graph type.
     *
     * @returns {Object}
     */
    getContext: function () {
        return {};
    },
    /**
     * Returns a title that may be displayed in the breadcrumb area.  For
     * example, the name of the record.
     *
     * note: this will be moved to AbstractAction
     *
     * @returns {string}
     */
    getTitle: function () {
        return this.displayName;
    },
    /**
     * The use of this method is discouraged.  It is still snakecased, because
     * it currently is used in many templates, but we will move to a simpler
     * mechanism as soon as we can.
     *
     * @deprecated
     * @param {string} action type of action, such as 'create', 'read', ...
     * @returns {boolean}
     */
    is_action_enabled: function (action) {
        return this.activeActions[action];
    },
    /**
     * Short helper method to reload the view
     *
     * @param {Object} [params] This object will simply be given to the update
     * @returns {Deferred}
     */
    reload: function (params) {
        return this.update(params || {});
    },
    /**
     * For views that require a pager, this method will be called to allow the
     * controller to instantiate and render a pager. Note that in theory, the
     * controller can actually render whatever he wants in the pager zone.  If
     * your view does not want a pager, just let this method empty.
     *
     * @param {jQuery Node} $node
     */
    renderPager: function ($node) {
    },
    /**
     * Same as renderPager, but for the 'sidebar' zone (the zone with the menu
     * dropdown in the control panel next to the buttons)
     *
     * @param {jQuery Node} $node
     */
    renderSidebar: function ($node) {
    },
    /**
     * This is the main entry point for the controller.  Changes from the search
     * view arrive in this method, and internal changes can sometimes also call
     * this method.  It is basically the way everything notifies the controller
     * that something has changed.
     *
     * The update method is responsible for fetching necessary data, then
     * updating the renderer and wait for the rendering to complete.
     *
     * @param {Object} params will be given to the model and to the renderer
     * @param {Object} [options]
     * @param {boolean} [options.reload=true] if true, the model will reload data
     *
     * @returns {Deferred}
     */
    update: function (params, options) {
        var self = this;
        var shouldReload = (options && 'reload' in options) ? options.reload : true;
        var def = shouldReload ? this.model.reload(this.handle, params) : $.when();
        // we check here that the updateIndex of the control panel hasn't changed
        // between the start of the update request and the moment the controller
        // asks the control panel to update itself ; indeed, it could happen that
        // another action/controller is executed during this one reloads itself,
        // and if that one finishes first, it replaces this controller in the DOM,
        // and this controller should no longer update the control panel.
        // note that this won't be necessary as soon as each controller will have
        // its own control panel
        var cpUpdateIndex = this.cp_bus && this.cp_bus.updateIndex;
        return this.dp.add(def).then(function (handle) {
            if (self.cp_bus && cpUpdateIndex !== self.cp_bus.updateIndex) {
                return;
            }
            self.handle = handle || self.handle; // update handle if we reloaded
            var state = self.model.get(self.handle);
            var localState = self.renderer.getLocalState();
            return self.dp.add(self.renderer.updateState(state, params)).then(function () {
                if (self.cp_bus && cpUpdateIndex !== self.cp_bus.updateIndex) {
                    return;
                }
                self.renderer.setLocalState(localState);
                self._update(state);
            });
        });
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * This method is the way a view can notifies the outside world that
     * something has changed.  The main use for this is to update the url, for
     * example with a new id.
     *
     * @private
     * @param {Object} [state] information that will be pushed to the outside
     *   world
     */
    _pushState: function (state) {
        this.trigger_up('push_state', {
            controllerID: this.controllerID,
            state: state || {},
        });
    },
    /**
     * Renders the control elements (buttons, pager and sidebar) of the current
     * view.
     *
     * @private
     * @returns {Object} an object containing the control panel jQuery elements
     */
    _renderControlPanelElements: function () {
        var elements = {};

        if (this.withControlPanel) {
            elements = {
                $buttons: $('<div>'),
                $sidebar: $('<div>'),
                $pager: $('<div>'),
            };

            this.renderButtons(elements.$buttons);
            this.renderSidebar(elements.$sidebar);
            this.renderPager(elements.$pager);
            // remove the unnecessary outer div
            elements = _.mapObject(elements, function($node) {
                return $node && $node.contents();
            });
            elements.$switch_buttons = this._renderSwitchButtons();
        }

        return elements;
    },
    /**
     * Renders the switch buttons and binds listeners on them.
     *
     * @private
     * @returns {jQuery}
     */
    _renderSwitchButtons: function () {
        var self = this;
        var views = _.filter(this.actionViews, {multiRecord: this.isMultiRecord});

        if (views.length <= 1) {
            return $();
        }

        var $switchButtons = $(QWeb.render('ControlPanel.SwitchButtons', {
            views: views,
        }));
        // create bootstrap tooltips
        _.each(views, function (view) {
            $switchButtons.filter('.o_cp_switch_' + view.type).tooltip();
        });
        // add onclick event listener
        $switchButtons.filter('button').click(_.debounce(function (event) {
            var viewType = $(event.target).data('view-type');
            self.trigger_up('switch_view', {view_type: viewType});
        }, 200, true));

        return $switchButtons;
    },
    /**
     * This method is called after each update or when the start method is
     * completed.
     *
     * Its primary use is to be used as a hook to update all parts of the UI,
     * besides the renderer.  For example, it may be used to enable/disable
     * some buttons in the control panel, such as the current graph type for a
     * graph view.
     *
     * @private
     * @param {Object} state the state given by the model
     * @returns {Deferred}
     */
    _update: function (state) {
        // AAB: update the control panel -> this will be moved elsewhere at some point
        var cpContent = _.extend({}, this.controlPanelElements);
        if (this.searchView) {
            _.extend(cpContent, {
                $searchview: this.searchView.$el,
                $searchview_buttons: this.searchView.$buttons,
            });
        }
        this.update_control_panel({
            active_view_selector: '.o_cp_switch_' + this.viewType,
            cp_content: cpContent,
            hidden: !this.withControlPanel,
            searchview: this.searchView,
            search_view_hidden: !this.searchable || this.searchviewHidden,
            groupable: this.groupable,
        });

        this._pushState();
        return $.when();
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * When an Odoo event arrives requesting a record to be opened, this method
     * gets the res_id, and request a switch view in the appropriate mode
     *
     * Note: this method seems wrong, it relies on the model being a basic model,
     * to get the res_id.  It should receive the res_id in the event data
     * @todo move this to basic controller?
     *
     * @private
     * @param {OdooEvent} event
     * @param {number} event.data.id The local model ID for the record to be
     *   opened
     * @param {string} [event.data.mode='readonly']
     */
    _onOpenRecord: function (event) {
        event.stopPropagation();
        var record = this.model.get(event.data.id, {raw: true});
        this.trigger_up('switch_view', {
            view_type: 'form',
            res_id: record.res_id,
            mode: event.data.mode || 'readonly',
            model: this.modelName,
        });
    },
    /**
     * When a user clicks on an <a> link with type="action", we need to actually
     * do the action. This kind of links is used a lot in no-content helpers.
     *
     * @private
     * @param {OdooEvent} event
     */
    _onActionClicked: function (event) {
        event.preventDefault();
        this.do_action(event.currentTarget.name);
    },
    /**
     * Intercepts the 'switch_view' event to add the controllerID into the data,
     * and lets the event bubble up.
     *
     * @param {OdooEvent} event
     */
    _onSwitchView: function (event) {
        event.data.controllerID = this.controllerID;
    },

});

return AbstractController;

});
Example #8
0
odoo.define('web_settings_dashboard', function (require) {
"use strict";

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var framework = require('web.framework');
var Widget = require('web.Widget');

var QWeb = core.qweb;
var _t = core._t;

var Dashboard = AbstractAction.extend({
    template: 'DashboardMain',

    init: function(){
        this.all_dashboards = ['apps', 'invitations', 'share', 'translations', 'company'];
        return this._super.apply(this, arguments);
    },

    start: function(){
        return this.load(this.all_dashboards);
    },

    load: function(dashboards){
        var self = this;
        var loading_done = new $.Deferred();
        this._rpc({route: '/web_settings_dashboard/data'})
            .then(function (data) {
                // Load each dashboard
                var all_dashboards_defs = [];
                _.each(dashboards, function(dashboard) {
                    var dashboard_def = self['load_' + dashboard](data);
                    if (dashboard_def) {
                        all_dashboards_defs.push(dashboard_def);
                    }
                });

                // Resolve loading_done when all dashboards defs are resolved
                $.when.apply($, all_dashboards_defs).then(function() {
                    loading_done.resolve();
                });
            });
        return loading_done;
    },

    load_apps: function(data){
        return  new DashboardApps(this, data.apps).replace(this.$('.o_web_settings_dashboard_apps'));
    },

    load_share: function(data){
        return new DashboardShare(this, data.share).replace(this.$('.o_web_settings_dashboard_share'));
    },

    load_invitations: function(data){
        return new DashboardInvitations(this, data.users_info).replace(this.$('.o_web_settings_dashboard_invitations'));
    },

    load_translations: function (data) {
        return new DashboardTranslations(this, data.translations).replace(this.$('.o_web_settings_dashboard_translations'));
    },

    load_company: function (data) {
        return new DashboardCompany(this, data.company).replace(this.$('.o_web_settings_dashboard_company'));
    },
});

var DashboardInvitations = Widget.extend({
    template: 'DashboardInvitations',
    events: {
        'click .o_web_settings_dashboard_invite': '_onClickInvite',
        'click .o_web_settings_dashboard_access_rights': 'on_access_rights_clicked',
        'click .o_web_settings_dashboard_user': 'on_user_clicked',
        'click .o_web_settings_dashboard_more': 'on_more',
        'click .o_badge_remove': '_onClickBadgeRemove',
        'keydown .o_user_emails': '_onKeydownUserEmails',
    },
    init: function(parent, data) {
        this.data = data;
        this.parent = parent;
        this.emails = [];
        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * Creates and appends badges for valid and unique email addresses
     *
     * @private
     */
    _createBadges: function () {
        var $userEmails = this.$('.o_user_emails');
        var value = $userEmails.val().trim();
        if (value) {
            // filter out duplicates
            var emails = _.uniq(value.split(/[ ,;\n]+/));

            // filter out invalid email addresses
            var invalidEmails = _.reject(emails, this._validateEmail.bind(this));
            if (invalidEmails.length) {
                this.do_warn(_.str.sprintf(_t('The following email addresses are invalid: %s.'), invalidEmails.join(', ')));
            }
            emails = _.difference(emails, invalidEmails);

            if (!this.data.resend_invitation) {
                // filter out already processed or pending addresses
                var pendingEmails = _.map(this.data.pending_users, function (info) {
                    return info[1];
                });
                var existingEmails = _.intersection(emails, this.emails.concat(pendingEmails));
                if (existingEmails.length) {
                    this.do_warn(_.str.sprintf(_t('The following email addresses already exist: %s.'), existingEmails.join(', ')));
                }
                emails = _.difference(emails, existingEmails);
            }

            // add valid email addresses, if any
            if (emails.length) {
                this.emails = this.emails.concat(emails);
                $userEmails.before(QWeb.render('EmailBadge', {emails: emails}));
                $userEmails.val('');
            }
        }
    },
    /**
     * Removes a given badge from the DOM, and its associated email address
     *
     * @private
     * @param {jQueryElement} $badge
     */
    _removeBadge: function ($badge) {
        var email = $badge.text().trim();
        this.emails = _.without(this.emails, email);
        $badge.remove();
    },
    /**
     * @private
     * @param {string} email
     * @returns {boolean} true iff the given email address is valid
     */
    _validateEmail: function (email) {
        var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,63}(?:\.[a-z]{2})?)$/i;
        return re.test(email);
    },
    on_access_rights_clicked: function (e) {
        var self = this;
        e.preventDefault();
        this.do_action('base.action_res_users', {
            on_reverse_breadcrumb: function(){ return self.reload();}
        });
    },
    on_user_clicked: function (e) {
        var self = this;
        e.preventDefault();
        var user_id = $(e.currentTarget).data('user-id');
        var action = {
            type: 'ir.actions.act_window',
            view_type: 'form',
            view_mode: 'form',
            res_model: 'res.users',
            views: [[this.data.user_form_view_id, 'form']],
            res_id: user_id,
        };
        this.do_action(action,{
            on_reverse_breadcrumb: function(){ return self.reload();}
        });
    },
    on_more: function(e) {
        var self = this;
        e.preventDefault();
        var action = {
            name: _t('Users'),
            type: 'ir.actions.act_window',
            view_type: 'form',
            view_mode: 'tree,form',
            res_model: 'res.users',
            domain: [['log_ids', '=', false]],
            context: {search_default_no_share: true},
            views: [[false, 'list'], [false, 'form']],
        };
        this.do_action(action,{
            on_reverse_breadcrumb: function(){ return self.reload();}
        });
    },
    reload:function(){
        return this.parent.load(['invitations']);
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onClickBadgeRemove: function (ev) {
        var $badge = $(ev.target).closest('.badge');
        this._removeBadge($badge);
    },
    /**
     * @private
     * @param {MouseEvent} ev
     */
    _onClickInvite: function (ev) {
        var self = this;
        this._createBadges();
        if (this.emails.length) {
            var $button = $(ev.target);
            $button.button('loading');
            this._rpc({
                model: 'res.users',
                method: 'web_dashboard_create_users',
                args: [this.emails],
            })
            .then(function () {
                self.reload();
            })
            .fail(function () {
                $button.button('reset');
            });
        }
    },
    /**
     * @private
     * @param {KeyboardEvent} ev
     */
     _onKeydownUserEmails: function (ev) {
        var $userEmails = this.$('.o_user_emails');
        var keyCodes = [$.ui.keyCode.TAB, $.ui.keyCode.COMMA, $.ui.keyCode.ENTER, $.ui.keyCode.SPACE];
        if (_.contains(keyCodes, ev.which)) {
            ev.preventDefault();
            this._createBadges();
        }
        // remove last badge on backspace
        if (ev.which === $.ui.keyCode.BACKSPACE && this.emails.length && !$userEmails.val()) {
            this._removeBadge(this.$('.o_web_settings_dashboard_invitation_form .badge:last'));
        }
    },
});

var DashboardApps = Widget.extend({

    template: 'DashboardApps',

    events: {
        'click .o_browse_apps': 'on_new_apps',
        'click .o_confirm_upgrade': 'confirm_upgrade',
    },

    init: function(parent, data){
        this.data = data;
        this.parent = parent;
        return this._super.apply(this, arguments);
    },

    start: function() {
        this._super.apply(this, arguments);
        if (odoo.db_info && _.last(odoo.db_info.server_version_info) !== 'e') {
            $(QWeb.render("DashboardEnterprise")).appendTo(this.$el);
        }
    },

    on_new_apps: function(){
        this.do_action('base.open_module_tree');
    },

    confirm_upgrade: function() {
        framework.redirect("https://www.odoo.com/odoo-enterprise/upgrade?num_users=" + (this.data.enterprise_users || 1));
    },
});

var DashboardShare = Widget.extend({
    template: 'DashboardShare',

    events: {
        'click .tw_share': 'share_twitter',
        'click .fb_share': 'share_facebook',
        'click .li_share': 'share_linkedin',
    },

    init: function (parent, data) {
        this.data = data;
        this.parent = parent;
        this.share_url = 'https://www.odoo.com';
        this.share_text = encodeURIComponent("I am using #Odoo - Awesome open source business apps.");
    },

    /**
     * @param {MouseEvent} ev
     */
    share_twitter: function (ev) {
        ev.preventDefault();
        var popup_url = _.str.sprintf( 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=%s %s',this.share_text,this.share_url);
        this.sharer(popup_url);
    },
    /**
     * @param {MouseEvent} ev
     */
    share_facebook: function (ev) {
        ev.preventDefault();
        var popup_url = _.str.sprintf('https://www.facebook.com/sharer/sharer.php?u=%s', encodeURIComponent(this.share_url));
        this.sharer(popup_url);
    },

    /**
     * @param {MouseEvent} ev
     */
    share_linkedin: function (ev) {
        ev.preventDefault();
        var popup_url = _.str.sprintf('http://www.linkedin.com/shareArticle?mini=true&url=%s&title=I am using odoo&summary=%s&source=www.odoo.com', encodeURIComponent(this.share_url), this.share_text);
        this.sharer(popup_url);
    },

    sharer: function (popup_url) {
        window.open(
            popup_url,
            'Share Dialog',
            'width=600,height=400'); // We have to add a size otherwise the window pops in a new tab
    }
});

var DashboardTranslations = Widget.extend({
    template: 'DashboardTranslations',

    events: {
        'click .o_load_translations': 'on_load_translations'
    },

    on_load_translations: function () {
        this.do_action('base.action_view_base_language_install');
    }

});

var DashboardCompany = Widget.extend({
    template: 'DashboardCompany',

    events: {
        'click .o_setup_company': 'on_setup_company'
    },

    init: function (parent, data) {
        this.data = data;
        this.parent = parent;
        this._super.apply(this, arguments);
    },

    on_setup_company: function () {
        var self = this;
        var action = {
            type: 'ir.actions.act_window',
            res_model: 'res.company',
            view_mode: 'form',
            view_type: 'form',
            views: [[false, 'form']],
            res_id: this.data.company_id
        };
        this.do_action(action, {
            on_reverse_breadcrumb: function () { return self.reload(); }
        });
    },

    reload: function () {
        return this.parent.load(['company']);
    }
});

core.action_registry.add('web_settings_dashboard.main', Dashboard);

return {
    Dashboard: Dashboard,
    DashboardApps: DashboardApps,
    DashboardInvitations: DashboardInvitations,
    DashboardShare: DashboardShare,
    DashboardTranslations: DashboardTranslations,
    DashboardCompany: DashboardCompany
};

});
Example #9
0
odoo.define('base_import.import', function (require) {
"use strict";

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var session = require('web.session');
var time = require('web.time');

var QWeb = core.qweb;
var _t = core._t;
var _lt = core._lt;
var StateMachine = window.StateMachine;

/**
 * Safari does not deal well at all with raw JSON data being
 * returned. As a result, we're going to cheat by using a
 * pseudo-jsonp: instead of getting JSON data in the iframe, we're
 * getting a ``script`` tag which consists of a function call and
 * the returned data (the json dump).
 *
 * The function is an auto-generated name bound to ``window``,
 * which calls back into the callback provided here.
 *
 * @param {Object} form the form element (DOM or jQuery) to use in the call
 * @param {Object} attributes jquery.form attributes object
 * @param {Function} callback function to call with the returned data
 */
function jsonp(form, attributes, callback) {
    attributes = attributes || {};
    var options = {jsonp: _.uniqueId('import_callback_')};
    window[options.jsonp] = function () {
        delete window[options.jsonp];
        callback.apply(null, arguments);
    };
    if ('data' in attributes) {
        _.extend(attributes.data, options);
    } else {
        _.extend(attributes, {data: options});
    }
    _.extend(attributes, {
        dataType: 'script',
    });
    $(form).ajaxSubmit(attributes);
}
function _make_option(term) { return {id: term, text: term }; }
function _from_data(data, term) {
    return _.findWhere(data, {id: term}) || _make_option(term);
}

/**
 * query returns a list of suggestion select2 objects, this function:
 *
 * * returns data exactly matching query by either id or text if those exist
 * * otherwise it returns a select2 option matching the term and any data
 *   option whose id or text matches (by substring)
 */
function dataFilteredQuery(q) {
    var suggestions = _.clone(this.data);
    if (q.term) {
        var exact = _.filter(suggestions, function (s) {
            return s.id === q.term || s.text === q.term;
        });
        if (exact.length) {
            suggestions = exact;
        } else {
            suggestions = [_make_option(q.term)].concat(_.filter(suggestions, function (s) {
                return s.id.indexOf(q.term) !== -1 || s.text.indexOf(q.term) !== -1
            }));
        }
    }
    q.callback({results: suggestions});
}

var DataImport = AbstractAction.extend({
    hasControlPanel: true,
    contentTemplate: 'ImportView',
    opts: [
        {name: 'encoding', label: _lt("Encoding:"), value: ''},
        {name: 'separator', label: _lt("Separator:"), value: ''},
        {name: 'quoting', label: _lt("Text Delimiter:"), value: '"'}
    ],
    parse_opts_formats: [
        {name: 'date_format', label: _lt("Date Format:"), value: ''},
        {name: 'datetime_format', label: _lt("Datetime Format:"), value: ''},
    ],
    parse_opts_separators: [
        {name: 'float_thousand_separator', label: _lt("Thousands Separator:"), value: ','},
        {name: 'float_decimal_separator', label: _lt("Decimal Separator:"), value: '.'}
    ],
    events: {
        // 'change .oe_import_grid input': 'import_dryrun',
        'change .oe_import_file': 'loaded_file',
        'change input.oe_import_has_header, .js_import_options input': 'settings_changed',
        'change input.oe_import_advanced_mode': function (e) {
            this.do_not_change_match = true;
            this['settings_changed']();
        },
        'click a.oe_import_toggle': function (e) {
            e.preventDefault();
            this.$('.oe_import_options').toggle();
        },
        'click .oe_import_report a.oe_import_report_count': function (e) {
            e.preventDefault();
            $(e.target).parent().toggleClass('oe_import_report_showmore');
        },
        'click .oe_import_moreinfo_action a': function (e) {
            e.preventDefault();
            // #data will parse the attribute on its own, we don't like
            // that sort of things
            var action = JSON.parse($(e.target).attr('data-action'));
            // FIXME: when JS-side clean_action
            action.views = _(action.views).map(function (view) {
                var id = view[0], type = view[1];
                return [
                    id,
                    type !== 'tree' ? type
                      : action.view_type === 'form' ? 'list'
                      : 'tree'
                ];
            });
            this.do_action(_.extend(action, {
                target: 'new',
                flags: {
                    search_view: true,
                    display_title: true,
                    pager: true,
                    list: {selectable: false}
                }
            }));
        },
    },
    init: function (parent, action) {
        this._super.apply(this, arguments);
        this.action_manager = parent;
        this.res_model = action.params.model;
        this.parent_context = action.params.context || {};
        // import object id
        this.id = null;
        this.session = session;
        this._title = _t('Import a File'); // Displayed in the breadcrumbs
        this.do_not_change_match = false;
    },
    /**
     * @override
     */
    willStart: function () {
        var self = this;
        var def = this._rpc({
            model: this.res_model,
            method: 'get_import_templates',
            context: this.parent_context,
        }).then(function (result) {
            self.importTemplates = result;
        });
        return $.when(this._super.apply(this, arguments), def);
    },
    start: function () {
        var self = this;
        this.$form = this.$('form');
        this.setup_encoding_picker();
        this.setup_separator_picker();
        this.setup_float_format_picker();
        this.setup_date_format_picker();

        return $.when(
            this._super(),
            self.create_model().done(function (id) {
                self.id = id;
                self.$('input[name=import_id]').val(id);

                self.renderButtons();
                var status = {
                    cp_content: {$buttons: self.$buttons},
                };
                self.updateControlPanel(status);
            })
        );
    },
    create_model: function() {
        return this._rpc({
                model: 'base_import.import',
                method: 'create',
                args: [{res_model: this.res_model}],
                kwargs: {context: session.user_context},
            });
    },
    renderButtons: function() {
        var self = this;
        this.$buttons = $(QWeb.render("ImportView.buttons", this));
        this.$buttons.filter('.o_import_validate').on('click', this.validate.bind(this));
        this.$buttons.filter('.o_import_import').on('click', this.import.bind(this));
        this.$buttons.filter('.o_import_file_reload').on('click', this.loaded_file.bind(this));
        this.$buttons.filter('.oe_import_file').on('click', function () {
            self.$('.o_content .oe_import_file').click();
        });
        this.$buttons.filter('.o_import_cancel').on('click', function(e) {
            e.preventDefault();
            self.exit();
        });
    },
    setup_encoding_picker: function () {
        this.$('input.oe_import_encoding').select2({
            width: '160px',
            data: _.map(('utf-8 utf-16 windows-1252 latin1 latin2 big5 gb18030 shift_jis windows-1251 koir8_r').split(/\s+/), _make_option),
            query: dataFilteredQuery,
            initSelection: function ($e, c) {
                return c(_make_option($e.val()));
            }
        });
    },
    setup_separator_picker: function () {
        var data = [
            {id: ',', text: _t("Comma")},
            {id: ';', text: _t("Semicolon")},
            {id: '\t', text: _t("Tab")},
            {id: ' ', text: _t("Space")}
        ];
        this.$('input.oe_import_separator').select2({
            width: '160px',
            data: data,
            query: dataFilteredQuery,
            // this is not provided to initSelection so can't use this.data
            initSelection: function ($e, c) {
                c(_from_data(data, $e.val()) || _make_option($e.val()))
            }
        });
    },
    setup_float_format_picker: function () {
        var data_decimal = [
            {id: ',', text: _t("Comma")},
            {id: '.', text: _t("Dot")},
        ];
        var data_digits = data_decimal.concat([{id: '', text: _t("No Separator")}]);
        this.$('input.oe_import_float_thousand_separator').select2({
            width: '160px',
            data: data_digits,
            query: dataFilteredQuery,
            initSelection: function ($e, c) {
                c(_from_data(data_digits, $e.val()) || _make_option($e.val()))
            }
        });
        this.$('input.oe_import_float_decimal_separator').select2({
            width: '160px',
            data: data_decimal,
            query: dataFilteredQuery,
            initSelection: function ($e, c) {
                c(_from_data(data_decimal, $e.val()) || _make_option($e.val()))
            }
        });
    },
    setup_date_format_picker: function () {
        var data = _([
            'YYYY-MM-DD',
            'DD/MM/YY',
            'DD/MM/YYYY',
            'DD-MM-YYYY',
            'DD-MMM-YY',
            'DD-MMM-YYYY',
            'MM/DD/YY',
            'MM/DD/YYYY',
            'MM-DD-YY',
            'MM-DD-YYYY',
            'DDMMYY',
            'DDMMYYYY',
            'YYMMDD',
            'YYYYMMDD',
            'YY/MM/DD',
            'YYYY/MM/DD',
            'MMDDYY',
            'MMDDYYYY',
        ]).map(_make_option);
        this.$('input.oe_import_date_format').select2({
            width: '160px',
            data: data,
            query: dataFilteredQuery,
            initSelection: function ($e, c) {
                c(_from_data(data, $e.val()) || _make_option($e.val()));
            }
        })
    },

    import_options: function () {
        var self = this;
        var options = {
            headers: this.$('input.oe_import_has_header').prop('checked'),
            advanced: this.$('input.oe_import_advanced_mode').prop('checked'),
            keep_matches: this.do_not_change_match,
            name_create_enabled_fields: {},
        };
        _(this.opts).each(function (opt) {
            options[opt.name] =
                self.$('input.oe_import_' + opt.name).val();
        });
        _(this.parse_opts_formats).each(function (opt) {
            options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_' + opt.name).val());
        });
        _(this.parse_opts_separators).each(function (opt) {
            options[opt.name] = self.$('input.oe_import_' + opt.name).val();
        });
        options['fields'] = [];
        if (this.do_not_change_match) {
            options['fields'] = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) {
                return $(el).select2('val') || false;
            }).get();
        }
        this.do_not_change_match = false;
        this.$('input.o_import_create_option').each(function () {
            var field = this.getAttribute('field');
            if (field) {
                options.name_create_enabled_fields[field] = this.checked;
            }
        });
        return options;
    },

    //- File & settings change section
    onfile_loaded: function () {
        this.$buttons.filter('.o_import_import, .o_import_validate, .o_import_file_reload').addClass('d-none');
        if (!this.$('input.oe_import_file').val()) { return this['settings_changed'](); }
        this.$('.oe_import_date_format').select2('val', '');
        this.$('.oe_import_datetime_format').val('');

        this.$form.removeClass('oe_import_preview oe_import_error');
        var import_toggle = false;
        var file = this.$('input.oe_import_file')[0].files[0];
        // some platforms send text/csv, application/csv, or other things if Excel is prevent
        if ((file.type && _.last(file.type.split('/')) === "csv") || ( _.last(file.name.split('.')) === "csv")) {
            import_toggle = true;
        }
        this.$form.find('.oe_import_box').toggle(import_toggle);
        jsonp(this.$form, {
            url: '/base_import/set_file'
        }, this.proxy('settings_changed'));
    },
    onpreviewing: function () {
        var self = this;
        this.$buttons.filter('.o_import_import, .o_import_validate, .o_import_file_reload').addClass('d-none');
        this.$form.addClass('oe_import_with_file');
        // TODO: test that write // succeeded?
        this.$form.removeClass('oe_import_preview_error oe_import_error');
        this.$form.toggleClass(
            'oe_import_noheaders text-muted',
            !this.$('input.oe_import_has_header').prop('checked'));
        this._rpc({
                model: 'base_import.import',
                method: 'parse_preview',
                args: [this.id, this.import_options()],
                kwargs: {context: session.user_context},
            }).done(function (result) {
                var signal = result.error ? 'preview_failed' : 'preview_succeeded';
                self[signal](result);
            });
    },
    onpreview_error: function (event, from, to, result) {
        this.$('.oe_import_options').show();
        this.$buttons.filter('.o_import_file_reload').removeClass('d-none');
        this.$form.addClass('oe_import_preview_error oe_import_error');
        this.$form.find('.oe_import_box, .oe_import_with_file').removeClass('d-none');
        this.$form.find('.o_view_nocontent').addClass('d-none');
        this.$('.oe_import_error_report').html(
                QWeb.render('ImportView.preview.error', result));
    },
    onpreview_success: function (event, from, to, result) {
        var self = this;
        this.$buttons.filter('.oe_import_file')
            .text(_t('Load New File'))
            .removeClass('btn-primary').addClass('btn-secondary')
            .blur();
        this.$buttons.filter('.o_import_import, .o_import_validate, .o_import_file_reload').removeClass('d-none');
        this.$form.find('.oe_import_box, .oe_import_with_file').removeClass('d-none');
        this.$form.find('.o_view_nocontent').addClass('d-none');
        this.$form.addClass('oe_import_preview');
        this.$('input.oe_import_advanced_mode').prop('checked', result.advanced_mode);
        this.$('.oe_import_grid').html(QWeb.render('ImportView.preview', result));

        if (result.headers.length === 1) {
            this.$('.oe_import_options').show();
            this.onresults(null, null, null, {'messages': [{
                type: 'warning',
                message: _t("A single column was found in the file, this often means the file separator is incorrect")
            }]});
        }

        // merge option values back in case they were updated/guessed
        _.each(['encoding', 'separator', 'float_thousand_separator', 'float_decimal_separator'], function (id) {
            self.$('.oe_import_' + id).select2('val', result.options[id])
        });
        this.$('.oe_import_date_format').select2('val', time.strftime_to_moment_format(result.options.date_format));
        this.$('.oe_import_datetime_format').val(time.strftime_to_moment_format(result.options.datetime_format));
        if (result.debug === false){
            this.$('.oe_import_tracking').hide();
            this.$('.oe_import_deferparentstore').hide();
        }

        var $fields = this.$('.oe_import_fields input');
        this.render_fields_matches(result, $fields);
        var data = this.generate_fields_completion(result);
        var item_finder = function (id, items) {
            items = items || data;
            for (var i=0; i < items.length; ++i) {
                var item = items[i];
                if (item.id === id) {
                    return item;
                }
                var val;
                if (item.children && (val = item_finder(id, item.children))) {
                    return val;
                }
            }
            return '';
        };
        $fields.each(function (k,v) {
            var filtered_data = self.generate_fields_completion(result, k);

            var $thing = $();
            var bind = function (d) {};
            if (session.debug) {
                $thing = $(QWeb.render('ImportView.create_record_option')).insertAfter(v).hide();
                bind = function (data) {
                    switch (data.type) {
                    case 'many2one': case 'many2many':
                        $thing.find('input').attr('field', data.id);
                        $thing.show();
                        break;
                    default:
                        $thing.find('input').attr('field', '').prop('checked', false);
                        $thing.hide();
                    }
                }
            }

            $(v).select2({
                allowClear: true,
                minimumInputLength: 0,
                data: filtered_data,
                initSelection: function (element, callback) {
                    var default_value = element.val();
                    if (!default_value) {
                        callback('');
                        return;
                    }

                    var data = item_finder(default_value);
                    bind(data);
                    callback(data);
                },
                placeholder: _t('Don\'t import'),
                width: 'resolve',
                dropdownCssClass: 'oe_import_selector'
            }).on('change', function (e) {
                bind(item_finder(e.currentTarget.value));
            });
        });
    },
    generate_fields_completion: function (root, index) {
        var basic = [];
        var regulars = [];
        var o2m = [];
        var headers_type = root.headers_type;
        function traverse(field, ancestors, collection, type) {
            var subfields = field.fields;
            var advanced_mode = self.$('input.oe_import_advanced_mode').prop('checked');
            var field_path = ancestors.concat(field);
            var label = _(field_path).pluck('string').join(' / ');
            var id = _(field_path).pluck('name').join('/');
            if (type === undefined || (type !== undefined && (type.indexOf('all') !== -1 || type.indexOf(field['type']) !== -1))){
                // If non-relational, m2o or m2m, collection is regulars
                if (!collection) {
                    if (field.name === 'id') {
                        collection = basic;
                    } else if (_.isEmpty(subfields)
                            || _.isEqual(_.pluck(subfields, 'name'), ['id', '.id'])) {
                        collection = regulars;
                    } else {
                        collection = o2m;
                    }
                }

                collection.push({
                    id: id,
                    text: label,
                    required: field.required,
                    type: field.type
                });

            }
            if (advanced_mode){
                for(var i=0, end=subfields.length; i<end; ++i) {
                    traverse(subfields[i], field_path, collection, type);
                }
            }
        }
        _(root.fields).each(function (field) {
            if (index === undefined) {
                traverse(field, []);
            }
            else {
                if (self.$('input.oe_import_advanced_mode').prop('checked')){
                    traverse(field, [], undefined, ['all']);
                }
                else {
                    traverse(field, [], undefined, headers_type[index]);
                }
            }
        });

        var cmp = function (field1, field2) {
            return field1.text.localeCompare(field2.text);

        };
        regulars.sort(cmp);
        o2m.sort(cmp);
        if (!_.isEmpty(regulars) && !_.isEmpty(o2m)){
            basic = basic.concat([
                { text: _t("Normal Fields"), children: regulars },
                { text: _t("Relation Fields"), children: o2m },
            ]);
        }
        else if (!_.isEmpty(regulars)) {
            basic = basic.concat(regulars);
        }
        else if (!_.isEmpty(o2m)) {
            basic = basic.concat(o2m);
        }
        return basic;
    },
    render_fields_matches: function (result, $fields) {
        if (_(result.matches).isEmpty()) { return; }
        $fields.each(function (index, input) {
            var match = result.matches[index];
            if (!match) { return; }

            var current_field = result;
            input.value = _(match).chain()
                .map(function (name) {
                    // WARNING: does both mapping and folding (over the
                    //          ``field`` iterator variable)
                    return current_field = _(current_field.fields).find(function (subfield) {
                        return subfield.name === name;
                    });
                })
                .pluck('name')
                .value()
                .join('/');
        });
    },

    //- import itself
    call_import: function (kwargs) {
        var fields = this.$('.oe_import_fields input.oe_import_match_field').map(function (index, el) {
            return $(el).select2('val') || false;
        }).get();
        var columns = this.$('.oe_import_grid-header .oe_import_grid-cell .o_import_header_name').map(function () {
            return $(this).text().trim().toLowerCase() || false;
        }).get();

        var tracking_disable = 'tracking_disable' in kwargs ? kwargs.tracking_disable : !this.$('#oe_import_tracking').prop('checked')
        var defer_parent_store = 'defer_parent_store' in kwargs ? kwargs.defer_parent_store : !!this.$('#oe_import_deferparentstore').prop('checked')
        delete kwargs.tracking_disable;
        delete kwargs.defer_parent_store;
        kwargs.context = _.extend(
            {}, this.parent_context,
            {tracking_disable: tracking_disable, defer_parent_store_computation: defer_parent_store}
        );
        return this._rpc({
                model: 'base_import.import',
                method: 'do',
                args: [this.id, fields, columns, this.import_options()],
                kwargs : kwargs,
            }).then(null, function (error, event) {
                // In case of unexpected exception, convert
                // "JSON-RPC error" to an import failure, and
                // prevent default handling (warning dialog)
                if (event) { event.preventDefault(); }

                var msg;
                if (error.data.type === 'xhrerror') {
                    var xhr = error.data.objects[0];
                    switch (xhr.status) {
                    case 504: // gateway timeout
                        msg = _t("Import timed out. Please retry. If you still encounter this issue, the file may be too big for the system's configuration, try to split it (import less records per file).");
                        break;
                    default:
                        msg = _t("An unknown issue occurred during import (possibly lost connection, data limit exceeded or memory limits exceeded). Please retry in case the issue is transient. If the issue still occurs, try to split the file rather than import it at once.");
                    }
                } else {
                    msg = (error.data.arguments && error.data.arguments[1] || error.data.arguments[0])
                        || error.message;
                }

                return $.when({'messages': [{
                    type: 'error',
                    record: false,
                    message: msg,
                }]});
            }) ;
    },
    onvalidate: function () {
        return this.call_import({ dryrun: true, tracking_disable: true })
            .done(this.proxy('validated'));
    },
    onimport: function () {
        var self = this;
        return this.call_import({ dryrun: false }).done(function (results) {
            var message = results.messages;
            if (!_.any(message, function (message) {
                    return message.type === 'error'; })) {
                self['import_succeeded'](results);
                return;
            }
            self['import_failed'](results);
        });
    },
    onimported: function (event, from, to, results) {
        this.do_notify(_t("Import completed"), _.str.sprintf(_t("%d records were successfully imported"), results.ids.length));
        this.exit();
    },
    exit: function () {
        this.trigger_up('history_back');
    },
    onresults: function (event, from, to, results) {
        var message = results.messages;
        var no_messages = _.isEmpty(message);
        if (no_messages) {
            message.push({
                type: 'info',
                message: _t("Everything seems valid.")
            });
        }
        // row indexes come back 0-indexed, spreadsheets
        // display 1-indexed.
        var offset = 1;
        // offset more if header
        if (this.import_options().headers) { offset += 1; }

        this.$form.addClass('oe_import_error');
        this.$('.oe_import_error_report').html(
            QWeb.render('ImportView.error', {
                errors: _(message).groupBy('message'),
                at: function (rows) {
                    var from = rows.from + offset;
                    var to = rows.to + offset;
                    if (from === to) {
                        return _.str.sprintf(_t("at row %d"), from);
                    }
                    return _.str.sprintf(_t("between rows %d and %d"),
                                         from, to);
                },
                more: function (n) {
                    return _.str.sprintf(_t("(%d more)"), n);
                },
                info: function (msg) {
                    if (typeof msg === 'string') {
                        return _.str.sprintf(
                            '<div class="oe_import_moreinfo oe_import_moreinfo_message">%s</div>',
                            _.str.escapeHTML(msg));
                    }
                    if (msg instanceof Array) {
                        return _.str.sprintf(
                            '<div class="oe_import_moreinfo oe_import_moreinfo_choices">%s <ul>%s</ul></div>',
                            _.str.escapeHTML(_t("Here are the possible values:")),
                            _(msg).map(function (msg) {
                                return '<li>'
                                    + _.str.escapeHTML(msg)
                                + '</li>';
                            }).join(''));
                    }
                    // Final should be object, action descriptor
                    return [
                        '<div class="oe_import_moreinfo oe_import_moreinfo_action">',
                            _.str.sprintf('<a href="#" data-action="%s">',
                                    _.str.escapeHTML(JSON.stringify(msg))),
                                _.str.escapeHTML(
                                    _t("Get all possible values")),
                            '</a>',
                        '</div>'
                    ].join('');
                },
            }));
    },
});
core.action_registry.add('import', DataImport);

// FSM-ize DataImport
StateMachine.create({
    target: DataImport.prototype,
    events: [
        { name: 'loaded_file',
          from: ['none', 'file_loaded', 'preview_error', 'preview_success', 'results'],
          to: 'file_loaded' },
        { name: 'settings_changed',
          from: ['file_loaded', 'preview_error', 'preview_success', 'results'],
          to: 'previewing' },
        { name: 'preview_failed', from: 'previewing', to: 'preview_error' },
        { name: 'preview_succeeded', from: 'previewing', to: 'preview_success' },
        { name: 'validate', from: 'preview_success', to: 'validating' },
        { name: 'validate', from: 'results', to: 'validating' },
        { name: 'validated', from: 'validating', to: 'results' },
        { name: 'import', from: ['preview_success', 'results'], to: 'importing' },
        { name: 'import_succeeded', from: 'importing', to: 'imported'},
        { name: 'import_failed', from: 'importing', to: 'results' }
    ],
});

return {
    DataImport: DataImport,
};

});
Example #10
0
odoo.define('web_settings_dashboard', function (require) {
"use strict";

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var framework = require('web.framework');
var Widget = require('web.Widget');

var QWeb = core.qweb;
var _t = core._t;

var Dashboard = AbstractAction.extend({
    template: 'DashboardMain',

    init: function(){
        this.all_dashboards = ['apps', 'invitations', 'share', 'translations', 'company'];
        return this._super.apply(this, arguments);
    },

    start: function(){
        return this.load(this.all_dashboards);
    },

    load: function(dashboards){
        var self = this;
        var loading_done = new $.Deferred();
        this._rpc({route: '/web_settings_dashboard/data'})
            .then(function (data) {
                // Load each dashboard
                var all_dashboards_defs = [];
                _.each(dashboards, function(dashboard) {
                    var dashboard_def = self['load_' + dashboard](data);
                    if (dashboard_def) {
                        all_dashboards_defs.push(dashboard_def);
                    }
                });

                // Resolve loading_done when all dashboards defs are resolved
                $.when.apply($, all_dashboards_defs).then(function() {
                    loading_done.resolve();
                });
            });
        return loading_done;
    },

    load_apps: function(data){
        return  new DashboardApps(this, data.apps).replace(this.$('.o_web_settings_dashboard_apps'));
    },

    load_share: function(data){
        return new DashboardShare(this, data.share).replace(this.$('.o_web_settings_dashboard_share'));
    },

    load_invitations: function(data){
        return new DashboardInvitations(this, data.users_info).replace(this.$('.o_web_settings_dashboard_invitations'));
    },

    load_translations: function (data) {
        return new DashboardTranslations(this, data.translations).replace(this.$('.o_web_settings_dashboard_translations'));
    },

    load_company: function (data) {
        return new DashboardCompany(this, data.company).replace(this.$('.o_web_settings_dashboard_company'));
    }
});

var DashboardInvitations = Widget.extend({
    template: 'DashboardInvitations',
    events: {
        'click .o_web_settings_dashboard_invitations': 'send_invitations',
        'click .o_web_settings_dashboard_access_rights': 'on_access_rights_clicked',
        'click .o_web_settings_dashboard_user': 'on_user_clicked',
        'click .o_web_settings_dashboard_more': 'on_more',
    },
    init: function(parent, data){
        this.data = data;
        this.parent = parent;
        return this._super.apply(this, arguments);
    },
    send_invitations: function(e){
        var self = this;
        var $target = $(e.currentTarget);
        var user_emails =  _.filter($(e.delegateTarget).find('#user_emails').val().split(/[\n, ]/), function(email){
            return email !== "";
        });
        var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,63}(?:\.[a-z]{2})?)$/i;
        var is_valid_emails = _.every(user_emails, function(email) {
            return re.test(email);
        });
        if (is_valid_emails) {
            // Disable button
            $target.prop('disabled', true);
            $target.find('i.fa-cog').removeClass('hidden');
            // Try to create user accountst
            this._rpc({
                    model: 'res.users',
                    method: 'web_dashboard_create_users',
                    args: [user_emails],
                })
                .then(function() {
                    self.reload();
                })
                .always(function() {
                    // Re-enable button
                    $(e.delegateTarget).find('.o_web_settings_dashboard_invitations').prop('disabled', false);
                    $(e.delegateTarget).find('i.fa-cog').addClass('hidden');
                });

        }
        else {
            this.do_warn(_t("Please provide valid email addresses"), "");
        }
    },
    on_access_rights_clicked: function (e) {
        var self = this;
        e.preventDefault();
        this.do_action('base.action_res_users', {
            on_reverse_breadcrumb: function(){ return self.reload();}
        });
    },
    on_user_clicked: function (e) {
        var self = this;
        e.preventDefault();
        var user_id = $(e.currentTarget).data('user-id');
        var action = {
            type: 'ir.actions.act_window',
            view_type: 'form',
            view_mode: 'form',
            res_model: 'res.users',
            views: [[this.data.user_form_view_id, 'form']],
            res_id: user_id,
        };
        this.do_action(action,{
            on_reverse_breadcrumb: function(){ return self.reload();}
        });
    },
    on_more: function(e) {
        var self = this;
        e.preventDefault();
        var action = {
            type: 'ir.actions.act_window',
            view_type: 'form',
            view_mode: 'tree,form',
            res_model: 'res.users',
            domain: [['log_ids', '=', false]],
            views: [[false, 'list'], [false, 'form']],
        };
        this.do_action(action,{
            on_reverse_breadcrumb: function(){ return self.reload();}
        });
    },
    reload:function(){
        return this.parent.load(['invitations']);
    },
});

var DashboardApps = Widget.extend({

    template: 'DashboardApps',

    events: {
        'click .o_browse_apps': 'on_new_apps',
        'click .o_confirm_upgrade': 'confirm_upgrade',
    },

    init: function(parent, data){
        this.data = data;
        this.parent = parent;
        return this._super.apply(this, arguments);
    },

    start: function() {
        this._super.apply(this, arguments);
        if (odoo.db_info && _.last(odoo.db_info.server_version_info) !== 'e') {
            $(QWeb.render("DashboardEnterprise")).appendTo(this.$el);
        }
    },

    on_new_apps: function(){
        this.do_action('base.open_module_tree');
    },

    confirm_upgrade: function() {
        framework.redirect("https://www.odoo.com/odoo-enterprise/upgrade?num_users=" + (this.data.enterprise_users || 1));
    },
});

var DashboardShare = Widget.extend({
    template: 'DashboardShare',

    events: {
        'click .tw_share': 'share_twitter',
        'click .fb_share': 'share_facebook',
        'click .li_share': 'share_linkedin',
    },

    init: function (parent, data) {
        this.data = data;
        this.parent = parent;
        this.share_url = 'https://www.odoo.com';
        this.share_text = encodeURIComponent("I am using #Odoo - Awesome open source business apps.");
    },

    /**
     * @param {MouseEvent} ev
     */
    share_twitter: function (ev) {
        ev.preventDefault();
        var popup_url = _.str.sprintf( 'https://twitter.com/intent/tweet?tw_p=tweetbutton&text=%s %s',this.share_text,this.share_url);
        this.sharer(popup_url);
    },
    /**
     * @param {MouseEvent} ev
     */
    share_facebook: function (ev) {
        ev.preventDefault();
        var popup_url = _.str.sprintf('https://www.facebook.com/sharer/sharer.php?u=%s', encodeURIComponent(this.share_url));
        this.sharer(popup_url);
    },

    /**
     * @param {MouseEvent} ev
     */
    share_linkedin: function (ev) {
        ev.preventDefault();
        var popup_url = _.str.sprintf('http://www.linkedin.com/shareArticle?mini=true&url=%s&title=I am using odoo&summary=%s&source=www.odoo.com', encodeURIComponent(this.share_url), this.share_text);
        this.sharer(popup_url);
    },

    sharer: function (popup_url) {
        window.open(
            popup_url,
            'Share Dialog',
            'width=600,height=400'); // We have to add a size otherwise the window pops in a new tab
    }
});

var DashboardTranslations = Widget.extend({
    template: 'DashboardTranslations',

    events: {
        'click .o_load_translations': 'on_load_translations'
    },

    on_load_translations: function () {
        this.do_action('base.action_view_base_language_install');
    }

});

var DashboardCompany = Widget.extend({
    template: 'DashboardCompany',

    events: {
        'click .o_setup_company': 'on_setup_company'
    },

    init: function (parent, data) {
        this.data = data;
        this.parent = parent;
        this._super.apply(this, arguments);
    },

    on_setup_company: function () {
        var self = this;
        var action = {
            type: 'ir.actions.act_window',
            res_model: 'res.company',
            view_mode: 'form',
            view_type: 'form',
            views: [[false, 'form']],
            res_id: this.data.company_id
        };
        this.do_action(action, {
            on_reverse_breadcrumb: function () { return self.reload(); }
        });
    },

    reload: function () {
        return this.parent.load(['company']);
    }
});

core.action_registry.add('web_settings_dashboard.main', Dashboard);

return {
    Dashboard: Dashboard,
    DashboardApps: DashboardApps,
    DashboardInvitations: DashboardInvitations,
    DashboardShare: DashboardShare,
    DashboardTranslations: DashboardTranslations,
    DashboardCompany: DashboardCompany
};

});
Example #11
0
odoo.define('account.ReconciliationClientAction', function (require) {
"use strict";

var AbstractAction = require('web.AbstractAction');
var ReconciliationModel = require('account.ReconciliationModel');
var ReconciliationRenderer = require('account.ReconciliationRenderer');
var core = require('web.core');


/**
 * Widget used as action for 'account.bank.statement' reconciliation
 */
var StatementAction = AbstractAction.extend({
    hasControlPanel: true,
    title: core._t('Bank Reconciliation'),
    contentTemplate: 'reconciliation',
    custom_events: {
        change_mode: '_onAction',
        change_filter: '_onAction',
        change_offset: '_onAction',
        change_partner: '_onAction',
        add_proposition: '_onAction',
        remove_proposition: '_onAction',
        update_proposition: '_onAction',
        create_proposition: '_onAction',
        getPartialAmount: '_onActionPartialAmount',
        quick_create_proposition: '_onAction',
        partial_reconcile: '_onAction',
        validate: '_onValidate',
        change_name: '_onChangeName',
        close_statement: '_onCloseStatement',
        load_more: '_onLoadMore',
        reload: 'reload',
    },
    events: {
        'change .reconciliation_search_input': '_onSearch',
    },
    config: _.extend({}, AbstractAction.prototype.config, {
        // used to instantiate the model
        Model: ReconciliationModel.StatementModel,
        // used to instantiate the action interface
        ActionRenderer: ReconciliationRenderer.StatementRenderer,
        // used to instantiate each widget line
        LineRenderer: ReconciliationRenderer.LineRenderer,
        // used context params
        params: ['statement_line_ids'],
        // number of statements/partners/accounts to display
        defaultDisplayQty: 10,
        // number of moves lines displayed in 'match' mode
        limitMoveLines: 15,
    }),

    /**
     * @override
     * @param {Object} params
     * @param {Object} params.context
     *
     */
    init: function (parent, params) {
        this._super.apply(this, arguments);
        this.action_manager = parent;
        this.params = params;
        this.model = new this.config.Model(this, {
            modelName: "account.reconciliation.widget",
            defaultDisplayQty: params.params && params.params.defaultDisplayQty || this.config.defaultDisplayQty,
            limitMoveLines: params.params && params.params.limitMoveLines || this.config.limitMoveLines,
        });
        this.widgets = [];
        // Adding values from the context is necessary to put this information in the url via the action manager so that
        // you can retrieve it if the person shares his url or presses f5
        _.each(params.params, function (value, name) {
            params.context[name] = name.indexOf('_ids') !== -1 ? _.map((value+'').split(','), parseFloat) : value;
        });
        params.params = {};
        _.each(this.config.params, function (name) {
            if (params.context[name]) {
                params.params[name] = params.context[name];
            }
        });
    },

    /**
     * instantiate the action renderer
     *
     * @override
     */
    willStart: function () {
        var self = this;
        var def = this.model.load(this.params.context).then(this._super.bind(this));
        return def.then(function () {
                var title = self.model.bank_statement_id  && self.model.bank_statement_id.display_name;
                self._setTitle(title);
                self.renderer = new self.config.ActionRenderer(self, self.model, {
                    'bank_statement_id': self.model.bank_statement_id,
                    'valuenow': self.model.valuenow,
                    'valuemax': self.model.valuemax,
                    'defaultDisplayQty': self.model.defaultDisplayQty,
                    'title': title,
                });
            });
    },

    reload: function() {
        // On reload destroy all rendered line widget, reload data and then rerender widget
        var self = this;
        _.each(this.widgets, function(widget) {
            widget.destroy();
        })
        this.widgets = [];
        this.model.reload()
            .then(function() {
                self.$('.o_reconciliation_lines').html('');
                self._renderLines();
                self._openFirstLine();
            });
    },

    /**
     * append the renderer and instantiate the line renderers
     *
     * @override
     */
    start: function () {
        var self = this;

        this.renderer.prependTo(self.$('.o_form_sheet'));
        this._renderLines();

        // No more lines to reconcile, trigger the rainbowman.
        var initialState = this.renderer._initialState;
        if(initialState.valuenow === initialState.valuemax){
            initialState.context = this.model.getContext();
            this.renderer.showRainbowMan(initialState);
        }else{
            // Create a notification if some lines has been reconciled automatically.
            if(initialState.valuenow > 0)
                this.renderer._renderNotifications(this.model.statement.notifications);
            this._openFirstLine();
        }

        return this._super.apply(this, arguments);
    },

    /**
     * update the control panel and breadcrumbs
     *
     * @override
     */
    do_show: function () {
        this._super.apply(this, arguments);
        if (this.action_manager) {
            this.updateControlPanel({clear: true});
            this.action_manager.do_push_state({
                action: this.params.tag,
                active_id: this.params.res_id,
            });
        }
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * @private
     * @param {string} handle
     * @returns {Widget} widget line
     */
    _getWidget: function (handle) {
        return _.find(this.widgets, function (widget) {return widget.handle===handle;});
    },

    /**
     *
     */
    _loadMore: function(qty) {
        var self = this;
        return this.model.loadMore(qty).then(function () {
            self._renderLines();
        });
    },
    /**
     * sitch to 'match' the first available line
     *
     * @private
     */
    _openFirstLine: function () {
        var self = this;

        var handle = _.compact(_.map(this.model.lines,  function (line, handle) {
                return line.reconciled ? null : handle;
            }))[0];
        if (handle) {
            var line = this.model.getLine(handle);
            this.model.changeMode(handle, 'match').always(function () {
                self._getWidget(handle).update(line);
            });
        }
        return handle;
    },
    /**
     * render line widget and append to view
     *
     * @private
     */
    _renderLines: function () {
        var self = this;
        var linesToDisplay = this.model.getStatementLines();
        _.each(linesToDisplay, function (line, handle) {
            var widget = new self.config.LineRenderer(self, self.model, line);
            widget.handle = handle;
            self.widgets.push(widget);
            widget.appendTo(self.$('.o_reconciliation_lines'));
        });
        if (this.model.hasMoreLines() === false) {
            this.renderer.hideLoadMoreButton(true);
        }
        else {
            this.renderer.hideLoadMoreButton(false);
        }
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * dispatch on the camelcased event name to model method then update the
     * line renderer with the new state. If the mode was switched from 'inactive'
     * to 'create' or 'match', the other lines switch to 'inactive' mode
     *
     * @private
     * @param {OdooEvent} event
     */
    _onAction: function (event) {
        var self = this;
        var handle = event.target.handle;
        var line = this.model.getLine(handle);
        var mode = line.mode;
        this.model[_.str.camelize(event.name)](handle, event.data.data).always(function () {
            self._getWidget(handle).update(line);
            if (mode === 'inactive' && line.mode !== 'inactive') {
                _.each(self.model.lines, function (line, _handle) {
                    if (line.mode !== 'inactive' && _handle !== handle) {
                        self.model.changeMode(_handle, 'inactive');
                        var widget = self._getWidget(_handle);
                        if (widget) {
                            widget.update(line);
                        }
                    }
                });
            }
        });
    },
    
    /**
     * @private
     * @param {OdooEvent} ev
     */
    _onSearch: function (ev) {
        var self = this;
        ev.stopPropagation();
        this.reload();
    },

    _onActionPartialAmount: function(event) {
        var self = this;
        var handle = event.target.handle;
        var line = this.model.getLine(handle);
        var amount = this.model.getPartialReconcileAmount(handle, event.data);
        self._getWidget(handle).updatePartialAmount(event.data.data, amount);
    },

    /**
     * call 'changeName' model method
     *
     * @private
     * @param {OdooEvent} event
     */
    _onChangeName: function (event) {
        var self = this;
        var title = event.data.data;
        this.model.changeName(title).then(function () {
            self.title = title;
            self.set("title", title);
            self.renderer.update({
                'valuenow': self.model.valuenow,
                'valuemax': self.model.valuemax,
                'title': title,
            });
        });
    },
    /**
     * call 'closeStatement' model method
     *
     * @private
     * @param {OdooEvent} event
     */
    _onCloseStatement: function (event) {
        var self = this;
        return this.model.closeStatement().then(function (result) {
            self.do_action({
                name: 'Bank Statements',
                res_model: 'account.bank.statement',
                res_id: result,
                views: [[false, 'form']],
                type: 'ir.actions.act_window',
                view_type: 'form',
                view_mode: 'form',
            });
        });
    },
    /**
     * Load more statement and render them
     *
     * @param {OdooEvent} event
     */
    _onLoadMore: function (event) {
        return this._loadMore(this.model.defaultDisplayQty);
    },
    /**
     * call 'validate' model method then destroy the
     * validated lines and update the action renderer with the new status bar 
     * values and notifications then open the first available line
     *
     * @private
     * @param {OdooEvent} event
     */
    _onValidate: function (event) {
        var self = this;
        var handle = event.target.handle;
        var method = 'validate';
        this.model[method](handle).then(function (result) {
            self.renderer.update({
                'valuenow': self.model.valuenow,
                'valuemax': self.model.valuemax,
                'title': self.title,
                'time': Date.now()-self.time,
                'notifications': result.notifications,
                'context': self.model.getContext(),
            });
            _.each(result.handles, function (handle) {
                self._getWidget(handle).destroy();
                var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
                self.widgets.splice(index, 1);
            });
            // Get number of widget and if less than constant and if there are more to laod, load until constant
            if (self.widgets.length < self.model.defaultDisplayQty 
                && self.model.valuemax - self.model.valuenow >= self.model.defaultDisplayQty) {
                var toLoad = self.model.defaultDisplayQty - self.widgets.length;
                self._loadMore(toLoad);
            }
            self._openFirstLine();
        });
    },
});


/**
 * Widget used as action for 'account.move.line' and 'res.partner' for the
 * manual reconciliation and mark data as reconciliate
 */
var ManualAction = StatementAction.extend({
    title: core._t('Journal Items to Reconcile'),
    config: _.extend({}, StatementAction.prototype.config, {
        Model: ReconciliationModel.ManualModel,
        ActionRenderer: ReconciliationRenderer.ManualRenderer,
        LineRenderer: ReconciliationRenderer.ManualLineRenderer,
        params: ['company_ids', 'mode', 'partner_ids', 'account_ids'],
        defaultDisplayQty: 30,
        limitMoveLines: 15,
    }),

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * call 'validate' model method then destroy the
     * reconcilied lines, update the not reconcilied and update the action
     * renderer with the new status bar  values and notifications then open the
     * first available line
     *
     * @private
     * @param {OdooEvent} event
     */
    _onValidate: function (event) {
        var self = this;
        var handle = event.target.handle;
        var method = 'validate';
        this.model[method](handle).then(function (result) {
            _.each(result.reconciled, function (handle) {
                self._getWidget(handle).destroy();
            });
            _.each(result.updated, function (handle) {
                self._getWidget(handle).update(self.model.getLine(handle));
            });
            self.renderer.update({
                valuenow: _.compact(_.invoke(self.widgets, 'isDestroyed')).length,
                valuemax: self.widgets.length,
                title: self.title,
                time: Date.now()-self.time,
            });
            if(!_.any(result.updated, function (handle) {
                return self.model.getLine(handle).mode !== 'inactive';
            })) {
                self._openFirstLine();
            }
        });
    },
});

core.action_registry.add('bank_statement_reconciliation_view', StatementAction);
core.action_registry.add('manual_reconciliation_view', ManualAction);

return {
    StatementAction: StatementAction,
    ManualAction: ManualAction,
};
});
Example #12
0
odoo.define('hr_attendance.kiosk_mode', function (require) {
"use strict";

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var Session = require('web.session');

var QWeb = core.qweb;


var KioskMode = AbstractAction.extend({
    events: {
        "click .o_hr_attendance_button_employees": function(){ this.do_action('hr_attendance.hr_employee_attendance_action_kanban'); },
    },

    start: function () {
        var self = this;
        core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
        self.session = Session;
        var def = this._rpc({
                model: 'res.company',
                method: 'search_read',
                args: [[['id', '=', this.session.company_id]], ['name']],
            })
            .then(function (companies){
                self.company_name = companies[0].name;
                self.company_image_url = self.session.url('/web/image', {model: 'res.company', id: self.session.company_id, field: 'logo',});
                self.$el.html(QWeb.render("HrAttendanceKioskMode", {widget: self}));
                self.start_clock();
            });
        return $.when(def, this._super.apply(this, arguments));
    },

    _onBarcodeScanned: function(barcode) {
        var self = this;
        this._rpc({
                model: 'hr.employee',
                method: 'attendance_scan',
                args: [barcode, ],
            })
            .then(function (result) {
                if (result.action) {
                    self.do_action(result.action);
                } else if (result.warning) {
                    self.do_warn(result.warning);
                }
            });
    },

    start_clock: function() {
        this.clock_start = setInterval(function() {this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'}));}, 500);
        // First clock refresh before interval to avoid delay
        this.$(".o_hr_attendance_clock").show().text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'}));
    },

    destroy: function () {
        core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
        clearInterval(this.clock_start);
        this._super.apply(this, arguments);
    },
});

core.action_registry.add('hr_attendance_kiosk_mode', KioskMode);

return KioskMode;

});
Example #13
0
odoo.define('hr_attendance.greeting_message', function (require) {
"use strict";

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');

var _t = core._t;


var GreetingMessage = AbstractAction.extend({
    template: 'HrAttendanceGreetingMessage',

    events: {
        "click .o_hr_attendance_button_dismiss": function() { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
    },

    init: function(parent, action) {
        var self = this;
        this._super.apply(this, arguments);
        this.activeBarcode = true;

        // if no correct action given (due to an erroneous back or refresh from the browser), we set the dismiss button to return
        // to the (likely) appropriate menu, according to the user access rights
        if(!action.attendance) {
            this.activeBarcode = false;
            this.getSession().user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group) {
                if(has_group) {
                    self.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
                } else {
                    self.next_action = 'hr_attendance.hr_attendance_action_my_attendances';
                }
            });
            return;
        }

        this.next_action = action.next_action || 'hr_attendance.hr_attendance_action_my_attendances';
        // no listening to barcode scans if we aren't coming from the kiosk mode (and thus not going back to it with next_action)
        if (this.next_action != 'hr_attendance.hr_attendance_action_kiosk_mode' && this.next_action.tag != 'hr_attendance_kiosk_mode') {
            this.activeBarcode = false;
        }

        this.attendance = action.attendance;
        // We receive the check in/out times in UTC
        // This widget only deals with display, which should be in browser's TimeZone
        this.attendance.check_in = this.attendance.check_in && moment.utc(this.attendance.check_in).local();
        this.attendance.check_out = this.attendance.check_out && moment.utc(this.attendance.check_out).local();
        this.previous_attendance_change_date = action.previous_attendance_change_date && moment.utc(action.previous_attendance_change_date).local();

        // check in/out times displayed in the greeting message template.
        this.format_time = 'HH:mm:ss';
        this.attendance.check_in_time = this.attendance.check_in && this.attendance.check_in.format(this.format_time);
        this.attendance.check_out_time = this.attendance.check_out && this.attendance.check_out.format(this.format_time);
        this.employee_name = action.employee_name;
        this.attendanceBarcode = action.barcode;
    },

    start: function() {
        if (this.attendance) {
            this.attendance.check_out ? this.farewell_message() : this.welcome_message();
        }
        if (this.activeBarcode) {
            core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
        }
    },

    welcome_message: function() {
        var self = this;
        var now = this.attendance.check_in.clone();
        this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);

        if (now.hours() < 5) {
            this.$('.o_hr_attendance_message_message').append(_t("Good night"));
        } else if (now.hours() < 12) {
            if (now.hours() < 8 && Math.random() < 0.3) {
                if (Math.random() < 0.75) {
                    this.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
                } else {
                    this.$('.o_hr_attendance_message_message').append(_t("First come, first served"));
                }
            } else {
                this.$('.o_hr_attendance_message_message').append(_t("Good morning"));
            }
        } else if (now.hours() < 17){
            this.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
        } else if (now.hours() < 23){
            this.$('.o_hr_attendance_message_message').append(_t("Good evening"));
        } else {
            this.$('.o_hr_attendance_message_message').append(_t("Good night"));
        }
        if(this.previous_attendance_change_date){
            var last_check_out_date = this.previous_attendance_change_date.clone();
            if(now - last_check_out_date > 24*7*60*60*1000){
                this.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
            } else {
                if(Math.random() < 0.02){
                    this.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!"));
                }
            }
        }
    },

    farewell_message: function() {
        var self = this;
        var now = this.attendance.check_out.clone();
        this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);

        if(this.previous_attendance_change_date){
            var last_check_in_date = this.previous_attendance_change_date.clone();
            if(now - last_check_in_date > 1000*60*60*12){
                this.$('.o_hr_attendance_warning_message').show().append(_t("<b>Warning! Last check in was over 12 hours ago.</b><br/>If this isn't right, please contact Human Resource staff"));
                clearTimeout(this.return_to_main_menu);
                this.activeBarcode = false;
            } else if(now - last_check_in_date > 1000*60*60*8){
                this.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
            }
        }

        if (now.hours() < 12) {
            this.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
        } else if (now.hours() < 14) {
            this.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
            if (Math.random() < 0.05) {
                this.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
            } else if (Math.random() < 0.06) {
                this.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
            }
        } else if (now.hours() < 17) {
            this.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
        } else {
            if (now.hours() < 18 && Math.random() < 0.2) {
                this.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
            } else {
                this.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));
            }
        }
    },

    _onBarcodeScanned: function(barcode) {
        var self = this;
        if (this.attendanceBarcode !== barcode){
            if (this.return_to_main_menu) {  // in case of multiple scans in the greeting message view, delete the timer, a new one will be created.
                clearTimeout(this.return_to_main_menu);
            }
            core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
            this._rpc({
                    model: 'hr.employee',
                    method: 'attendance_scan',
                    args: [barcode, ],
                })
                .then(function (result) {
                    if (result.action) {
                        self.do_action(result.action);
                    } else if (result.warning) {
                        self.do_warn(result.warning);
                        setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
                    }
                }, function () {
                    setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
                });
        }
    },

    destroy: function () {
        core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
        clearTimeout(this.return_to_main_menu);
        this._super.apply(this, arguments);
    },
});

core.action_registry.add('hr_attendance_greeting_message', GreetingMessage);

return GreetingMessage;

});
Example #14
0
odoo.define('mail.chat_discuss', function (require) {
"use strict";

var ChatThread = require('mail.ChatThread');
var composer = require('mail.composer');

var AbstractAction = require('web.AbstractAction');
var config = require('web.config');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
var dom = require('web.dom');
var pyeval = require('web.pyeval');
var SearchView = require('web.SearchView');
var session = require('web.session');

var QWeb = core.qweb;
var _t = core._t;

/**
 * Widget : Invite People to Channel Dialog
 *
 * Popup containing a 'many2many_tags' custom input to select multiple partners.
 * Searches user according to the input, and triggers event when selection is
 * validated.
 */
var PartnerInviteDialog = Dialog.extend({
    dialog_title: _t('Invite people'),
    template: "mail.PartnerInviteDialog",

    /**
     * @override
     * @param {integer|string} channelID id of the channel,
     *      a string for static channels (e.g. 'channel_inbox').
     */
    init: function (parent, title, channelID) {
        this.channelID = channelID;

        this._super(parent, {
            title: title,
            size: "medium",
            buttons: [{
                text: _t("Invite"),
                close: true,
                classes: "btn-primary",
                click: this._addChannel.bind(this),
            }],
        });
    },
    /**
     * @override
     */
    start: function () {
        var self = this;
        this.$input = this.$('.o_mail_chat_partner_invite_input');
        this.$input.select2({
            width: '100%',
            allowClear: true,
            multiple: true,
            formatResult: function (item) {
                var status = QWeb.render('mail.chat.UserStatus', {status: item.im_status});
                return $('<span>').text(item.text).prepend(status);
            },
            query: function (query) {
                self.call('chat_manager', 'searchPartner', query.term, 20)
                    .then(function (partners) {
                        query.callback({
                            results: _.map(partners, function (partner) {
                                return _.extend(partner, { text: partner.label });
                            }),
                        });
                    });
            }
        });
        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * @private
     * @returns {$.Promise}
     */
    _addChannel: function () {
        var self = this;
        var data = this.$input.select2('data');
        if (data.length >= 1) {
            return this._rpc({
                    model: 'mail.channel',
                    method: 'channel_invite',
                    args: [this.channelID],
                    kwargs: {partner_ids: _.pluck(data, 'id')},
                }).then(function () {
                    var names = _.escape(_.pluck(data, 'text').join(', '));
                    var notification = _.str.sprintf(_t('You added <b>%s</b> to the conversation.'), names);
                    self.do_notify(_t('New people'), notification);
                    // Clear the membersDeferred to fetch again the partner
                    // when getMentionPartnerSuggestions from the chatManager is triggered
                    var channel = self.call('chat_manager', 'getChannel', self.channelID);
                    delete channel.membersDeferred;
                });
        }
    },
});

var Discuss = AbstractAction.extend(ControlPanelMixin, {
    template: 'mail.discuss',
    custom_events: {
        search: '_onSearch',
    },
    events: {
        'blur .o_mail_add_channel input': '_onAddChannelBlur',
        'click .o_mail_annoying_notification_bar .fa-close': '_onCloseNotificationBar',
        'click .o_mail_chat_channel_item': '_onChannelClicked',
        'click .o_mail_open_channels': '_onPublicChannelsClick',
        'click .o_mail_partner_unpin': '_onUnpinChannel',
        'click .o_mail_channel_settings': '_onChannelSettingsClicked',
        'click .o_mail_request_permission': '_onRequestNotificationPermission',
        'click .o_mail_sidebar_title .o_add': '_onAddChannel',
        'keydown': '_onKeydown',
    },

    /**
     * @override
     */
    init: function (parent, action, options) {
        this._super.apply(this, arguments);
        this.action_manager = parent;
        this.dataset = new data.DataSetSearch(this, 'mail.message');
        this.domain = [];
        this.action = action;
        this.options = options || {};
        this.channelsScrolltop = {};
        this.throttledUpdateChannels = _.throttle(this._updateChannels.bind(this), 100, { leading: false });
        this.notification_bar = (window.Notification && window.Notification.permission === "default");
        this.selected_message = null;
        this.composerStates = {};
        this.defaultChannelID = this.options.active_id ||
                                 this.action.context.active_id ||
                                 this.action.params.default_active_id ||
                                 'channel_inbox';
    },
    /**
     * @override
     */
    willStart: function () {
        var self = this;
        var viewID = this.action && this.action.search_view_id && this.action.search_view_id[0];
        var def = this
            .loadFieldView(this.dataset, viewID, 'search')
            .then(function (fields_view) {
                self.fields_view = fields_view;
            });
        return $.when(this._super(), this.call('chat_manager', 'isReady'), def);
    },
    /**
     * @override
     */
    start: function () {
        var self = this;
        var defaultChannel = this.call('chat_manager', 'getChannel', this.defaultChannelID) ||
                                this.call('chat_manager', 'getChannel', 'channel_inbox');

        this.basicComposer = new composer.BasicComposer(this, {mention_partners_restricted: true});
        this.extendedComposer = new composer.ExtendedComposer(this, {mention_partners_restricted: true});
        this.basicComposer.on('post_message', this, this._onPostMessage);
        this.basicComposer.on('input_focused', this, this._onComposerFocused);
        this.extendedComposer.on('post_message', this, this._onPostMessage);
        this.extendedComposer.on('input_focused', this, this._onComposerFocused);

        var defs = [];
        defs.push(this._renderButtons());
        defs.push(this._renderThread());
        defs.push(this.basicComposer.appendTo(this.$('.o_mail_chat_content')));
        defs.push(this.extendedComposer.appendTo(this.$('.o_mail_chat_content')));
        defs.push(this._renderSearchView());

        return $.when.apply($, defs)
            .then(this._setChannel.bind(this, defaultChannel))
            .then(this._updateChannels.bind(this))
            .then(function () {
                self._startListening();
                self.thread.$el.on("scroll", null, _.debounce(function () {
                    if (self.thread.is_at_bottom()) {
                        self.call('chat_manager', 'markChannelAsSeen', self.channel);
                    }
                }, 100));
            });
    },
    /**
     * @override
     */
    do_show: function () {
        this._super.apply(this, arguments);
        this._updateControlPanel();
        this.action_manager.do_push_state({
            action: this.action.id,
            active_id: this.channel.id,
        });
    },
    /**
     * @override
     */
    destroy: function () {
        if (this.$buttons) {
            this.$buttons.off().destroy();
        }
        this._super.apply(this, arguments);
    },
    /**
     * @override
     */
    on_attach_callback: function () {
        this.call('chat_manager', 'getChatBus').trigger('discuss_open', true);
        if (this.channel) {
            this.thread.scroll_to({offset: this.channelsScrolltop[this.channel.id]});
        }
    },
    /**
     * @override
     */
    on_detach_callback: function () {
        this.call('chat_manager', 'getChatBus').trigger('discuss_open', false);
        this.channelsScrolltop[this.channel.id] = this.thread.get_scrolltop();
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * @private
     * @returns {$.Promise}
     */
    _fetchAndRenderThread: function () {
        var self = this;
        return this.call('chat_manager', 'getMessages', {
                channelID: this.channel.id,
                domain: this.domain
            }).then(function (messages) {
                self.thread.render(messages, self._getThreadRenderingOptions(messages));
                self._updateButtonStatus(messages.length === 0);
            });
    },
    /**
     * @private
     * @param {Object} messages
     * @returns {Object}
     */
    _getThreadRenderingOptions: function (messages) {
        // Compute position of the 'New messages' separator, only once when joining
        // a channel to keep it in the thread when new messages arrive
        if (_.isUndefined(this.messages_separator_position)) {
            if (!this.unread_counter) {
                this.messages_separator_position = false; // no unread message -> don't display separator
            } else {
                var msg = this.call('chat_manager', 'getLastSeenMessage', this.channel);
                this.messages_separator_position = msg ? msg.id : 'top';
            }
        }
        return {
            channel_id: this.channel.id,
            display_load_more: !this.call('chat_manager', 'isAllHistoryLoaded', this.channel, this.domain),
            display_needactions: this.channel.display_needactions,
            messages_separator_position: this.messages_separator_position,
            squash_close_messages: this.channel.type !== 'static' && !this.channel.mass_mailing,
            display_empty_channel: !messages.length && !this.domain.length,
            display_no_match: !messages.length && this.domain.length,
            display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox",
            display_email_icon: false,
            display_reply_icon: true,
        };
    },
    /**
     * Load more messages for the current thread
     *
     * @private
     * @returns {$.Promise}
     */
    _loadMoreMessages: function () {
        var self = this;
        var oldestMsgID = this.$('.o_thread_message').first().data('messageId');
        var oldestMsgSelector = '.o_thread_message[data-message-id="' + oldestMsgID + '"]';
        var offset = -dom.getPosition(document.querySelector(oldestMsgSelector)).top;
        return this.call('chat_manager', 'getMessages', {
                channelID: this.channel.id,
                domain: this.domain,
                loadMore: true
            })
            .then(function (messages) {
                if (self.messages_separator_position === 'top') {
                    self.messages_separator_position = undefined; // reset value to re-compute separator position
                }
                self.thread.render(messages, self._getThreadRenderingOptions(messages));
                offset += dom.getPosition(document.querySelector(oldestMsgSelector)).top;
                self.thread.scroll_to({offset: offset});
            });
    },
    /**
     * Binds handlers on the given $input to make them autocomplete and/or
     * create channels.
     *
     * @private
     * @param {JQuery} $input the input to prepare
     * @param {string} type the type of channel to create ('dm', 'public' or
     *   'private')
     */
    _prepareAddChannelInput: function ($input, type) {
        var self = this;
        if (type === 'public') {
            $input.autocomplete({
                source: function (request, response) {
                    self.last_search_val = _.escape(request.term);
                    self._searchChannel(self.last_search_val).done(function (result){
                        result.push({
                            'label':  _.str.sprintf('<strong>'+_t("Create %s")+'</strong>', '<em>"#'+self.last_search_val+'"</em>'),
                            'value': '_create',
                        });
                        response(result);
                    });
                },
                select: function (event, ui) {
                    if (self.last_search_val) {
                        if (ui.item.value === '_create') {
                            self.call('chat_manager', 'createChannel', self.last_search_val, "public");
                        } else {
                            self.call('chat_manager', 'joinChannel', ui.item.id);
                        }
                    }
                },
                focus: function (event) {
                    event.preventDefault();
                },
                html: true,
            });
        } else if (type === 'private') {
            $input.on('keyup', this, function (event) {
                var name = _.escape($(event.target).val());
                if (event.which === $.ui.keyCode.ENTER && name) {
                    self.call('chat_manager', 'createChannel', name, "private");
                }
            });
        } else if (type === 'dm') {
            $input.autocomplete({
                source: function (request, response) {
                    self.last_search_val = _.escape(request.term);
                    self.call('chat_manager', 'searchPartner', self.last_search_val, 10).done(response);
                },
                select: function (event, ui) {
                    var partner_id = ui.item.id;
                    var dm = self.call('chat_manager', 'getDmFromPartnerID', partner_id);
                    if (dm) {
                        self._setChannel(dm);
                    } else {
                        self.call('chat_manager', 'createChannel', partner_id, "dm");
                    }
                    // clear the input
                    $(this).val('');
                    return false;
                },
                focus: function (event) {
                    event.preventDefault();
                },
            });
        }
    },
    /**
     * @private
     */
    _renderButtons: function () {
        this.$buttons = $(QWeb.render("mail.chat.ControlButtons", {debug: session.debug}));
        this.$buttons.find('button').css({display:"inline-block"});
        this.$buttons.on('click', '.o_mail_chat_button_invite', this._onInviteButtonClicked.bind(this));
        this.$buttons.on('click', '.o_mail_chat_button_mark_read', this._onMarkAllReadClicked.bind(this));
        this.$buttons.on('click', '.o_mail_chat_button_unstar_all', this._onUnstarAllClicked.bind(this));
    },
    /**
     * @private
     * @returns {Deferred}
     */
    _renderSearchView: function () {
        var self = this;
        var options = {
            $buttons: $("<div>"),
            action: this.action,
            disable_groupby: true,
        };
        this.searchview = new SearchView(this, this.dataset, this.fields_view, options);
        return this.searchview.appendTo($("<div>")).then(function () {
            self.$searchview_buttons = self.searchview.$buttons.contents();
            // manually call do_search to generate the initial domain and filter
            // the messages in the default channel
            self.searchview.do_search();
        });
    },
    /**
     * @private
     * @param {Object} options
     * @returns {JQuery}
     */
    _renderSidebar: function (options) {
        return $(QWeb.render("mail.chat.Sidebar", options));
    },
    /**
     * @private
     * @param {string} template
     * @param {Object} context rendering context
     * @param {integer} [timeout=20000] the delay before the snackbar disappears
     */
    _renderSnackbar: function (template, context, timeout) {
        if (this.$snackbar) {
            this.$snackbar.remove();
        }
        timeout = timeout || 20000;
        this.$snackbar = $(QWeb.render(template, context));
        this.$('.o_mail_chat_content').append(this.$snackbar);
        // Hide snackbar after [timeout] milliseconds (by default, 20s)
        var $snackbar = this.$snackbar;
        setTimeout(function () { $snackbar.fadeOut(); }, timeout);
    },
    /**
     * Renders, binds events and appends a thread widget.
     *
     * @private
     * @returns {Deferred}
     */
    _renderThread: function () {
        var self = this;
        this.thread = new ChatThread(this, {display_help: true});

        this.thread.on('redirect', this, function (resModel, resID) {
            self.call('chat_manager', 'redirect', resModel, resID, self._setChannel.bind(self));
        });
        this.thread.on('redirect_to_channel', this, function (channelID) {
            self.call('chat_manager', 'joinChannel', channelID).then(this._setChannel.bind(this));
        });
        this.thread.on('load_more_messages', this, this._loadMoreMessages);
        this.thread.on('mark_as_read', this, function (messageID) {
            self.call('chat_manager', 'markAsRead', [messageID]);
        });
        this.thread.on('toggle_star_status', this, function (messageID) {
            self.call('chat_manager', 'toggleStarStatus', messageID);
        });
        this.thread.on('select_message', this, this._selectMessage);
        this.thread.on('unselect_message', this, this._unselectMessage);

        return this.thread.appendTo(this.$('.o_mail_chat_content'));
    },
    /**
     * Restores the scroll position and composer state of the current channel
     *
     * @private
     */
    _restoreChannelState: function () {
        var $newMessagesSeparator = this.$('.o_thread_new_messages_separator');
        if ($newMessagesSeparator.length) {
            this.thread.$el.scrollTo($newMessagesSeparator);
        } else {
            var newChannelScrolltop = this.channelsScrolltop[this.channel.id];
            this.thread.scroll_to({offset: newChannelScrolltop});
        }
        this._restoreComposerState(this.channel);
    },
    /**
     * @private
     * @param {Object} channel
     */
    _restoreComposerState: function (channel) {
        if (channel.type === 'static') {
            return; // no composer in static channels
        }
        var composer = channel.mass_mailing ? this.extendedComposer : this.basicComposer;
        var composerState = this.composerStates[channel.uuid];
        if (composerState) {
            composer.setState(composerState);
        }
    },
    /**
     * @private
     * @param {string} searchVal
     * @returns {$.Promise<Array>}
     */
    _searchChannel: function (searchVal){
        return this._rpc({
                model: 'mail.channel',
                method: 'channel_search_to_join',
                args: [searchVal]
            })
            .then(function (result){
                var values = [];
                _.each(result, function (channel){
                    var escaped_name = _.escape(channel.name);
                    values.push(_.extend(channel, {
                        'value': escaped_name,
                        'label': escaped_name,
                    }));
                });
                return values;
            });
    },
    /**
     * @private
     * @param {integer} messageID
     */
    _selectMessage: function (messageID) {
        this.$el.addClass('o_mail_selection_mode');
        var message = this.call('chat_manager', 'getMessage', messageID);;
        this.selected_message = message;
        var subject = "Re: " + message.record_name;
        this.extendedComposer.set_subject(subject);

        if (this.channel.type !== 'static') {
            this.basicComposer.do_hide();
        }
        this.extendedComposer.do_show();

        this.thread.scroll_to({id: messageID, duration: 200, only_if_necessary: true});
        this.extendedComposer.focus('body');
    },
    /**
     * @private
     * @param {Object} channel
     * @returns {$.Promise}
     */
    _setChannel: function (channel) {
        var self = this;

        // Store scroll position and composer state of the previous channel
        this._storeChannelState();

        this.channel = channel;
        this.messages_separator_position = undefined; // reset value on channel change
        this.unread_counter = this.channel.unread_counter;
        this.last_seen_message_id = this.channel.last_seen_message_id;
        if (this.$snackbar) {
            this.$snackbar.remove();
        }

        this.action.context.active_id = channel.id;
        this.action.context.active_ids = [channel.id];

        return this._fetchAndRenderThread().then(function () {
            // Mark channel's messages as read and clear needactions
            if (channel.type !== 'static') {
                self.call('chat_manager', 'markChannelAsSeen', channel);
            }
            // Restore scroll position and composer of the new current channel
            self._restoreChannelState();

            // Update control panel before focusing the composer, otherwise focus is on the searchview
            self.set("title", '#' + channel.name);
            self._updateControlPanel();
            self._updateControlPanelButtons(channel);

            // Display and focus the adequate composer, and unselect possibly selected message
            // to prevent sending messages as reply to that message
            self._unselectMessage();

            self.action_manager.do_push_state({
                action: self.action.id,
                active_id: self.channel.id,
            });
        });
    },
    /**
     * Binds handlers on chatManager events
     *
     * @private
     */
    _startListening: function () {
        var chatBus = this.call('chat_manager', 'getChatBus');
        chatBus.on('open_channel', this, this._setChannel);
        chatBus.on('new_message', this, this._onNewMessage);
        chatBus.on('update_message', this, this._onMessageUpdated);
        chatBus.on('new_channel', this, this._onNewChannel);
        chatBus.on('anyone_listening', this, function (channel, query) {
            query.is_displayed = query.is_displayed ||
                                (channel.id === this.channel.id && this.thread.is_at_bottom());
        });
        chatBus.on('unsubscribe_from_channel', this, this._onChannelLeft);
        chatBus.on('update_needaction', this, this.throttledUpdateChannels);
        chatBus.on('update_starred', this, this.throttledUpdateChannels);
        chatBus.on('update_channel_unread_counter', this, this.throttledUpdateChannels);
        chatBus.on('update_dm_presence', this, this.throttledUpdateChannels);
        chatBus.on('activity_updated', this, this.throttledUpdateChannels);
    },
    /**
     * Stores the scroll position and composer state of the current channel
     *
     * @private
     */
    _storeChannelState: function () {
        if (this.channel) {
            this.channelsScrolltop[this.channel.id] = this.thread.get_scrolltop();
            this._storeComposerState(this.channel);
        }
    },
    /**
     * @private
     * @param {Object} channel
     */
    _storeComposerState: function (channel) {
        if (channel.type === 'static') {
            return; // no composer in static channels
        }
        var composer = channel.mass_mailing ? this.extendedComposer : this.basicComposer;
        this.composerStates[channel.uuid] = composer.getState();
        composer.clear_composer();
    },
    /**
     * @private
     */
    _unselectMessage: function () {
        this.basicComposer.do_toggle(this.channel.type !== 'static' && !this.channel.mass_mailing);
        this.extendedComposer.do_toggle(this.channel.type !== 'static' && this.channel.mass_mailing);

        if (!config.device.touch) {
            var composer = this.channel.mass_mailing ? this.extendedComposer : this.basicComposer;
            composer.focus();
        }
        this.$el.removeClass('o_mail_selection_mode');
        this.thread.unselect();
        this.selected_message = null;
    },
    /**
     * Renders the mainside bar with current channels
     *
     * @private
     */
    _updateChannels: function () {
        var self = this;
        var $sidebar = this._renderSidebar({
            active_channel_id: this.channel ? this.channel.id: undefined,
            channels: this.call('chat_manager', 'getChannels'),
            needaction_counter: this.call('chat_manager', 'getNeedactionCounter'),
            starred_counter: this.call('chat_manager', 'getStarredCounter'),
        });
        this.$(".o_mail_chat_sidebar").html($sidebar.contents());
        _.each(['dm', 'public', 'private'], function (type) {
            var $input = self.$('.o_mail_add_channel[data-type=' + type + '] input');
            self._prepareAddChannelInput($input, type);
        });
    },
    /**
     * @private
     * @param {boolean} disabled
     * @param {string} type
     */
    _updateButtonStatus: function (disabled, type) {
        if (this.channel.id === "channel_inbox") {
            this.$buttons
                .find('.o_mail_chat_button_mark_read')
                .toggleClass('disabled', disabled);
            // Display Rainbowman when all inbox messages are read through
            // 'MARK ALL READ' or marking last inbox message as read
            if (disabled && type === 'mark_as_read') {
                this.trigger_up('show_effect', {
                    message: _t('Congratulations, your inbox is empty!'),
                    type: 'rainbow_man',
                });
            }
        }
        if (this.channel.id === "channel_starred") {
            this.$buttons
                .find('.o_mail_chat_button_unstar_all')
                .toggleClass('disabled', disabled);
        }
    },
    /**
     * @private
     */
    _updateControlPanel: function () {
        this.update_control_panel({
            cp_content: {
                $buttons: this.$buttons,
                $searchview: this.searchview.$el,
                $searchview_buttons: this.$searchview_buttons,
            },
            searchview: this.searchview,
        });
    },
    /**
     * Updates the control panel buttons visibility based on channel type
     *
     * @private
     * @param {Object} channel
     */
    _updateControlPanelButtons: function (channel) {
        // Hide 'unsubscribe' button in state channels and DM and channels with group-based subscription
        this.$buttons
            .find('.o_mail_chat_button_invite, .o_mail_chat_button_settings')
            .toggle(channel.type !== "dm" && channel.type !== 'static');
        this.$buttons
            .find('.o_mail_chat_button_mark_read')
            .toggle(channel.id === "channel_inbox")
            .removeClass("o_hidden");
        this.$buttons
            .find('.o_mail_chat_button_unstar_all')
            .toggle(channel.id === "channel_starred")
            .removeClass("o_hidden");

        this.$('.o_mail_chat_channel_item')
            .removeClass('o_active')
            .filter('[data-channel-id=' + channel.id + ']')
            .removeClass('o_unread_message')
            .addClass('o_active');
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * @private
     * @param {MouseEvent} event
     */
    _onAddChannel: function (event) {
        event.preventDefault();
        var type = $(event.target).data("type");
        this.$('.o_mail_add_channel[data-type=' + type + ']')
            .show()
            .find("input").focus();
    },
    /**
     * @private
     */
    _onAddChannelBlur: function () {
        this.$('.o_mail_add_channel').hide();
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onChannelClicked: function (event) {
        event.preventDefault();
        var channelID = $(event.currentTarget).data('channel-id');
        var channel = this.call('chat_manager', 'getChannel', channelID);
        this._setChannel(channel);
    },
    /**
     * @private
     * @param {integer|string} channelID
     */
    _onChannelLeft: function (channelID) {
        if (this.channel.id === channelID) {
            var channel = this.call('chat_manager', 'getChannel', 'channel_inbox');
            this._setChannel(channel);
        }
        this._updateChannels();
        delete this.channelsScrolltop[channelID];
    },
    /**
     * @private
     */
    _onComposerFocused: function () {
        var composer = this.channel.mass_mailing ? this.extendedComposer : this.basicComposer;
        var commands = this.call('chat_manager', 'getCommands', this.channel);
        var partners = this.call('chat_manager', 'getMentionPartnerSuggestions', this.channel);
        composer.mention_set_enabled_commands(commands);
        composer.mention_set_prefetched_partners(partners);
    },
    /**
     * @private
     */
    _onMarkAllReadClicked: function () {
        this.call('chat_manager', 'markAllAsRead', this.channel, this.domain);
    },
    /**
     * @private
     * @param {Object} message
     * @param {string} type the channel type
     */
    _onMessageUpdated: function (message, type) {
        var self = this;
        var currentChannelID = this.channel.id;
        if ((currentChannelID === "channel_starred" && !message.is_starred) ||
            (currentChannelID === "channel_inbox" && !message.is_needaction)) {
                this.call('chat_manager', 'getMessages', {
                        channelID: this.channel.id,
                        domain: this.domain
                }).then(function (messages) {
                    var options = self._getThreadRenderingOptions(messages);
                    self.thread.remove_message_and_render(message.id, messages, options)
                        .then(function () {
                            self._updateButtonStatus(messages.length === 0, type);
                        });
                });
        } else if (_.contains(message.channel_ids, currentChannelID)) {
            this._fetchAndRenderThread();
        }
    },
    /**
     * @private
     * @param {Object} channel
     */
    _onNewChannel: function (channel) {
        this._updateChannels();
        if (channel.autoswitch) {
            this._setChannel(channel);
        }
    },
    /**
     * @private
     * @param {Object} message
     */
    _onNewMessage: function (message) {
        var self = this;
        if (_.contains(message.channel_ids, this.channel.id)) {
            if (this.channel.type !== 'static' && this.thread.is_at_bottom()) {
                this.call('chat_manager', 'markChannelAsSeen', this.channel);
            }
            var should_scroll = this.thread.is_at_bottom();
            this._fetchAndRenderThread().then(function () {
                if (should_scroll) {
                    self.thread.scroll_to({id: message.id});
                }
            });
        }
        // Re-render sidebar to indicate that there is a new message in the corresponding channels
        this._updateChannels();
        // Dump scroll position of channels in which the new message arrived
        this.channelsScrolltop = _.omit(this.channelsScrolltop, message.channel_ids);
    },
    /**
     * @private
     * @param {Object} message
     */
    _onPostMessage: function (message) {
        var self = this;
        var options = this.selected_message ? {} : {channelID: this.channel.id};
        if (this.selected_message) {
            message.subtype = this.selected_message.is_note ? 'mail.mt_note': 'mail.mt_comment';
            message.subtype_id = false;
            message.message_type = 'comment';
            message.content_subtype = 'html';
        }
        this.call('chat_manager', 'postMessage', message, options)
            .then(function () {
                if (self.selected_message) {
                    self._renderSnackbar('mail.chat.MessageSentSnackbar', {record_name: self.selected_message.record_name}, 5000);
                    self._unselectMessage();
                } else {
                    self.thread.scroll_to();
                }
            })
            .fail(function () {
                // todo: display notification
            });
    },
    /**
     * @private
     */
    _onPublicChannelsClick: function () {
        this.do_action({
            name: _t('Public Channels'),
            type: 'ir.actions.act_window',
            res_model: "mail.channel",
            views: [[false, 'kanban'], [false, 'form']],
            domain: [['public', '!=', 'private']],
        }, {
            on_reverse_breadcrumb: this.on_reverse_breadcrumb,
        });
    },
    /**
     * @private
     */
    _onCloseNotificationBar: function () {
        this.$(".o_mail_annoying_notification_bar").slideUp();
    },
    /**
     * @private
     */
    _onInviteButtonClicked: function () {
        var title = _.str.sprintf(_t('Invite people to #%s'), this.channel.name);
        new PartnerInviteDialog(this, title, this.channel.id).open();
    },
    /**
     * @private
     * @param {KeyEvent} event
     */
    _onKeydown: function (event) {
        if (event.which === $.ui.keyCode.ESCAPE && this.selected_message) {
            this._unselectMessage();
        }
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onRequestNotificationPermission: function (event) {
        var self = this;
        event.preventDefault();
        this.$(".o_mail_annoying_notification_bar").slideUp();
        var def = window.Notification && window.Notification.requestPermission();
        if (def) {
            def.then(function (value) {
                if (value !== 'granted') {
                    self.call('bus_service', 'sendNotification', self, _t('Permission denied'),
                        _t('Odoo will not have the permission to send native notifications on this device.'));
                } else {
                    self.call('bus_service', 'sendNotification', self, _t('Permission granted'),
                        _t('Odoo has now the permission to send you native notifications on this device.'));
                }
            });
        }
    },
    /**
     * @private
     * @param {OdooEvent}
     */
    _onSearch: function (event) {
        event.stopPropagation();
        var session = this.getSession();
        var result = pyeval.eval_domains_and_contexts({
            domains: event.data.domains,
            contexts: [session.user_context],
        });
        this.domain = result.domain;
        if (this.channel) {
            // initially (when _onSearch is called manually), there is no
            // channel set yet, so don't try to fetch and render the thread as
            // this will be done as soon as the default channel is set
            this._fetchAndRenderThread();
        }
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onChannelSettingsClicked: function (event) {
        var channelID = $(event.target).data("channel-id");
        this.do_action({
            type: 'ir.actions.act_window',
            res_model: "mail.channel",
            res_id: channelID,
            views: [[false, 'form']],
            target: 'current'
        });
    },
    /**
     * @private
     * @param {MouseEvent} event
     */
    _onUnpinChannel: function (event) {
        event.stopPropagation();
        var channelID = $(event.target).data("channel-id");
        var channel = this.call('chat_manager', 'getChannel', channelID);
        this.call('chat_manager', 'unsubscribe', channel);
    },
    /**
     * @private
     */
    _onUnstarAllClicked: function () {
        this.call('chat_manager', 'unstarAll');
    },
});

core.action_registry.add('mail.chat.instant_messaging', Discuss);

return Discuss;

});
Example #15
0
odoo.define('hr_attendance.kiosk_confirm', function (require) {
"use strict";

var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var QWeb = core.qweb;


var KioskConfirm = AbstractAction.extend({
    events: {
        "click .o_hr_attendance_back_button": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
        "click .o_hr_attendance_sign_in_out_icon": function () {
            var self = this;
            this.$('.o_hr_attendance_sign_in_out_icon').attr("disabled", "disabled");
            this._rpc({
                    model: 'hr.employee',
                    method: 'attendance_manual',
                    args: [[this.employee_id], this.next_action],
                })
                .then(function(result) {
                    if (result.action) {
                        self.do_action(result.action);
                    } else if (result.warning) {
                        self.do_warn(result.warning);
                        self.$('.o_hr_attendance_sign_in_out_icon').removeAttr("disabled");
                    }
                });
        },
        'click .o_hr_attendance_pin_pad_button_0': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 0); },
        'click .o_hr_attendance_pin_pad_button_1': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 1); },
        'click .o_hr_attendance_pin_pad_button_2': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 2); },
        'click .o_hr_attendance_pin_pad_button_3': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 3); },
        'click .o_hr_attendance_pin_pad_button_4': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 4); },
        'click .o_hr_attendance_pin_pad_button_5': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 5); },
        'click .o_hr_attendance_pin_pad_button_6': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 6); },
        'click .o_hr_attendance_pin_pad_button_7': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 7); },
        'click .o_hr_attendance_pin_pad_button_8': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 8); },
        'click .o_hr_attendance_pin_pad_button_9': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 9); },
        'click .o_hr_attendance_pin_pad_button_C': function() { this.$('.o_hr_attendance_PINbox').val(''); },
        'click .o_hr_attendance_pin_pad_button_ok': function() {
            var self = this;
            this.$('.o_hr_attendance_pin_pad_button_ok').attr("disabled", "disabled");
            this._rpc({
                    model: 'hr.employee',
                    method: 'attendance_manual',
                    args: [[this.employee_id], this.next_action, this.$('.o_hr_attendance_PINbox').val()],
                })
                .then(function(result) {
                    if (result.action) {
                        self.do_action(result.action);
                    } else if (result.warning) {
                        self.do_warn(result.warning);
                        self.$('.o_hr_attendance_PINbox').val('');
                        setTimeout( function() { self.$('.o_hr_attendance_pin_pad_button_ok').removeAttr("disabled"); }, 500);
                    }
                });
        },
    },

    init: function (parent, action) {
        this._super.apply(this, arguments);
        this.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
        this.employee_id = action.employee_id;
        this.employee_name = action.employee_name;
        this.employee_state = action.employee_state;
    },

    start: function () {
        var self = this;
        this.getSession().user_has_group('hr_attendance.group_hr_attendance_use_pin').then(function(has_group){
            self.use_pin = has_group;
            self.$el.html(QWeb.render("HrAttendanceKioskConfirm", {widget: self}));
            self.start_clock();
        });
        return self._super.apply(this, arguments);
    },

    start_clock: function () {
        this.clock_start = setInterval(function() {this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'}));}, 500);
        // First clock refresh before interval to avoid delay
        this.$(".o_hr_attendance_clock").show().text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'}));
    },

    destroy: function () {
        clearInterval(this.clock_start);
        this._super.apply(this, arguments);
    },
});

core.action_registry.add('hr_attendance_kiosk_confirm', KioskConfirm);

return KioskConfirm;

});
Example #16
0
odoo.define('website.backend.dashboard', function (require) {
'use strict';

var AbstractAction = require('web.AbstractAction');
var ajax = require('web.ajax');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var Dialog = require('web.Dialog');
var field_utils = require('web.field_utils');
var session = require('web.session');
var web_client = require('web.web_client');

var _t = core._t;
var QWeb = core.qweb;

var Dashboard = AbstractAction.extend(ControlPanelMixin, {
    template: 'website.WebsiteDashboardMain',
    cssLibs: [
        '/web/static/lib/nvd3/nv.d3.css'
    ],
    jsLibs: [
        '/web/static/lib/nvd3/d3.v3.js',
        '/web/static/lib/nvd3/nv.d3.js',
        '/web/static/src/js/libs/nvd3.js'
    ],
    events: {
        'click .js_link_analytics_settings': 'on_link_analytics_settings',
        'click .o_dashboard_action': 'on_dashboard_action',
        'click .o_dashboard_action_form': 'on_dashboard_action_form',
    },

    init: function(parent, context) {
        this._super(parent, context);

        this.date_range = 'week';  // possible values : 'week', 'month', year'
        this.date_from = moment().subtract(1, 'week');
        this.date_to = moment();

        this.dashboards_templates = ['website.dashboard_header', 'website.dashboard_content'];
        this.graphs = [];
    },

    willStart: function() {
        var self = this;
        return $.when(ajax.loadLibs(this), this._super()).then(function() {
            return self.fetch_data();
        });
    },

    start: function() {
        var self = this;
        return this._super().then(function() {
            self.update_cp();
            self.render_graphs();
            self.$el.parent().addClass('oe_background_grey');
        });
    },

    /**
     * Fetches dashboard data
     */
    fetch_data: function() {
        var self = this;
        return this._rpc({
            route: '/website/fetch_dashboard_data',
            params: {
                date_from: this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(),
                date_to: this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(),
            },
        }).done(function(result) {
            self.data = result;
            self.dashboards_data = result.dashboards;
            self.currency_id = result.currency_id;
            self.groups = result.groups;
        });
    },

    on_link_analytics_settings: function(ev) {
        ev.preventDefault();

        var self = this;
        var dialog = new Dialog(this, {
            size: 'medium',
            title: _t('Google Analytics'),
            $content: QWeb.render('website.ga_dialog_content', {
                ga_key: this.dashboards_data.visits.ga_client_id,
                ga_analytics_key: this.dashboards_data.visits.ga_analytics_key,
            }),
            buttons: [
                {
                    text: _t("Save"),
                    classes: 'btn-primary',
                    close: true,
                    click: function() {
                        var ga_client_id = dialog.$el.find('input[name="ga_client_id"]').val();
                        var ga_analytics_key = dialog.$el.find('input[name="ga_analytics_key"]').val();
                        self.on_save_ga_client_id(ga_client_id, ga_analytics_key);
                    },
                },
                {
                    text: _t("Cancel"),
                    close: true,
                },
            ],
        }).open();
    },

    on_save_ga_client_id: function(ga_client_id, ga_analytics_key) {
        var self = this;
        return this._rpc({
            route: '/website/dashboard/set_ga_data',
            params: {
                'ga_client_id': ga_client_id,
                'ga_analytics_key': ga_analytics_key,
            },
        }).then(function (result) {
            if (result.error) {
                self.do_warn(result.error.title, result.error.message);
                return;
            }
            self.on_date_range_button('week');
        });
    },

    render_dashboards: function() {
        var self = this;
        _.each(this.dashboards_templates, function(template) {
            self.$('.o_website_dashboard').append(QWeb.render(template, {widget: self}));
        });
    },

    render_graph: function(div_to_display, chart_values) {
        this.$(div_to_display).empty();

        var self = this;
        nv.addGraph(function() {
            var chart = nv.models.lineChart()
                .x(function(d) { return self.getDate(d); })
                .y(function(d) { return self.getValue(d); })
                .forceY([0]);
            chart
                .useInteractiveGuideline(true)
                .showLegend(false)
                .showYAxis(true)
                .showXAxis(true);

            var tick_values = self.getPrunedTickValues(chart_values[0].values, 5);

            chart.xAxis
                .tickFormat(function(d) { return d3.time.format("%m/%d/%y")(new Date(d)); })
                .tickValues(_.map(tick_values, function(d) { return self.getDate(d); }))
                .rotateLabels(-45);

            chart.interactiveLayer.tooltip.contentGenerator(function(data) {
                return QWeb.render('website.SalesChartTooltip', {
                    format: field_utils.format,
                    chartData: data,
                    dateRange: self.date_range
                });
            });

            chart.yAxis
                .tickFormat(d3.format('.02f'));

            var svg = d3.select(div_to_display)
                .append("svg");

            svg
                .attr("height", '24em')
                .datum(chart_values)
                .call(chart);

            nv.utils.windowResize(chart.update);
            return chart;
        });
    },

    render_graphs: function() {
        var self = this;
        _.each(this.graphs, function(e) {
            if (self.groups[e.group]) {
                self.render_graph('.o_graph_' + e.name, self.dashboards_data[e.name].graph);
            }
        });
        this.render_graph_analytics(this.dashboards_data.visits.ga_client_id);
    },

    render_graph_analytics: function(client_id) {
        if (!this.dashboards_data.visits || !this.dashboards_data.visits.ga_client_id) {
          return;
        }

        this.load_analytics_api();

        var $analytics_components = this.$('.js_analytics_components');
        this.addLoader($analytics_components);

        var self = this;
        gapi.analytics.ready(function() {

            $analytics_components.empty();
            // 1. Authorize component
            var $analytics_auth = $('<div>').addClass('col-lg-12');
            window.onOriginError = function () {
                $analytics_components.find('.js_unauthorized_message').remove();
                self.display_unauthorized_message($analytics_components, 'not_initialized');
            };
            gapi.analytics.auth.authorize({
                container: $analytics_auth[0],
                clientid: client_id
            });

            $analytics_auth.appendTo($analytics_components);

            self.handle_analytics_auth($analytics_components);
            gapi.analytics.auth.on('signIn', function() {
                delete window.onOriginError;
                self.handle_analytics_auth($analytics_components);
            });

        });
    },

    on_date_range_button: function(date_range) {
        if (date_range === 'week') {
            this.date_range = 'week';
            this.date_from = moment().subtract(1, 'weeks');
        } else if (date_range === 'month') {
            this.date_range = 'month';
            this.date_from = moment().subtract(1, 'months');
        } else if (date_range === 'year') {
            this.date_range = 'year';
            this.date_from = moment().subtract(1, 'years');
        } else {
            console.log('Unknown date range. Choose between [week, month, year]');
            return;
        }

        var self = this;
        $.when(this.fetch_data()).then(function() {
            self.$('.o_website_dashboard').empty();
            self.render_dashboards();
            self.render_graphs();
        });

    },

    on_reverse_breadcrumb: function() {
        var self = this;
        web_client.do_push_state({});
        this.update_cp();
        this.fetch_data().then(function() {
            self.$('.o_website_dashboard').empty();
            self.render_dashboards();
            self.render_graphs();
        });
    },

    on_dashboard_action: function (ev) {
        ev.preventDefault();
        var $action = $(ev.currentTarget);
        var additional_context = {};
        if (this.date_range === 'week') {
            additional_context = {search_default_week: true};
        } else if (this.date_range === 'month') {
            additional_context = {search_default_month: true};
        } else if (this.date_range === 'year') {
            additional_context = {search_default_year: true};
        }
        this.do_action($action.attr('name'), {
            additional_context: additional_context,
            on_reverse_breadcrumb: this.on_reverse_breadcrumb
        });
    },

    on_dashboard_action_form: function (ev) {
        ev.preventDefault();
        var $action = $(ev.currentTarget);
        this.do_action({
            name: $action.attr('name'),
            res_model: $action.data('res_model'),
            res_id: $action.data('res_id'),
            views: [[false, 'form']],
            type: 'ir.actions.act_window',
        }, {
            on_reverse_breadcrumb: this.on_reverse_breadcrumb
        });
    },

    update_cp: function() {
        var self = this;
        if (!this.$searchview) {
            this.$searchview = $(QWeb.render("website.DateRangeButtons", {
                widget: this,
            }));
            this.$searchview.click('button.js_date_range', function(ev) {
                self.on_date_range_button($(ev.target).data('date'));
                $(this).find('button.js_date_range.active').removeClass('active');
                $(ev.target).addClass('active');
            });
        }
        this.update_control_panel({
            cp_content: {
                $searchview: this.$searchview,
                $buttons: QWeb.render("website.GoToButtons"),
            },
        });
    },

    // Loads Analytics API
    load_analytics_api: function() {
        var self = this;
        if (!("gapi" in window)) {
            (function(w,d,s,g,js,fjs){
                g=w.gapi||(w.gapi={});g.analytics={q:[],ready:function(cb){this.q.push(cb);}};
                js=d.createElement(s);fjs=d.getElementsByTagName(s)[0];
                js.src='https://apis.google.com/js/platform.js';
                fjs.parentNode.insertBefore(js,fjs);js.onload=function(){g.load('analytics');};
            }(window,document,'script'));
            gapi.analytics.ready(function() {
                self.analytics_create_components();
            });
        }
    },

    handle_analytics_auth: function($analytics_components) {
        $analytics_components.find('.js_unauthorized_message').remove();

        // Check if the user is authenticated and has the right to make API calls
        if (!gapi.analytics.auth.getAuthResponse()) {
            this.display_unauthorized_message($analytics_components, 'not_connected');
        } else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics') === -1) {
            this.display_unauthorized_message($analytics_components, 'no_right');
        } else {
            this.make_analytics_calls($analytics_components);
        }
    },

    display_unauthorized_message: function($analytics_components, reason) {
        $analytics_components.prepend($(QWeb.render('website.unauthorized_analytics', {reason: reason})));
    },

    make_analytics_calls: function($analytics_components) {
        // 2. ActiveUsers component
        var $analytics_users = $('<div>');
        var activeUsers = new gapi.analytics.ext.ActiveUsers({
            container: $analytics_users[0],
            pollingInterval: 10,
        });
        $analytics_users.appendTo($analytics_components);

        // 3. View Selector
        var $analytics_view_selector = $('<div>').addClass('col-lg-12 o_properties_selection');
        var viewSelector = new gapi.analytics.ViewSelector({
            container: $analytics_view_selector[0],
        });
        viewSelector.execute();
        $analytics_view_selector.appendTo($analytics_components);

        // 4. Chart graph
        var start_date = '7daysAgo';
        if (this.date_range === 'month') {
            start_date = '30daysAgo';
        } else if (this.date_range === 'year') {
            start_date = '365daysAgo';
        }
        var $analytics_chart_2 = $('<div>').addClass('col-lg-6 col-12');
        var breakdownChart = new gapi.analytics.googleCharts.DataChart({
            query: {
                'dimensions': 'ga:date',
                'metrics': 'ga:sessions',
                'start-date': start_date,
                'end-date': 'yesterday'
            },
            chart: {
                type: 'LINE',
                container: $analytics_chart_2[0],
                options: {
                    title: 'All',
                    width: '100%'
                }
            }
        });
        $analytics_chart_2.appendTo($analytics_components);

        // 5. Chart table
        var $analytics_chart_1 = $('<div>').addClass('col-lg-6 col-12');
        var mainChart = new gapi.analytics.googleCharts.DataChart({
            query: {
                'dimensions': 'ga:medium',
                'metrics': 'ga:sessions',
                'sort': '-ga:sessions',
                'max-results': '6'
            },
            chart: {
                type: 'TABLE',
                container: $analytics_chart_1[0],
                options: {
                    width: '100%'
                }
            }
        });
        $analytics_chart_1.appendTo($analytics_components);

        // Events handling & animations

        var table_row_listener;

        viewSelector.on('change', function(ids) {
            var options = {query: {ids: ids}};
            activeUsers.set({ids: ids}).execute();
            mainChart.set(options).execute();
            breakdownChart.set(options).execute();

            if (table_row_listener) { google.visualization.events.removeListener(table_row_listener); }
        });

        mainChart.on('success', function(response) {
            var chart = response.chart;
            var dataTable = response.dataTable;

            table_row_listener = google.visualization.events.addListener(chart, 'select', function() {
                var options;
                if (chart.getSelection().length) {
                    var row =  chart.getSelection()[0].row;
                    var medium =  dataTable.getValue(row, 0);
                    options = {
                        query: {
                            filters: 'ga:medium==' + medium,
                        },
                        chart: {
                            options: {
                                title: medium,
                            }
                        }
                    };
                } else {
                    options = {
                        chart: {
                            options: {
                                title: 'All',
                            }
                        }
                    };
                    delete breakdownChart.get().query.filters;
                }
                breakdownChart.set(options).execute();
            });
        });

        // Add CSS animation to visually show the when users come and go.
        activeUsers.once('success', function() {
            var element = this.container.firstChild;
            var timeout;

            this.on('change', function(data) {
                element = this.container.firstChild;
                var animationClass = data.delta > 0 ? 'is-increasing' : 'is-decreasing';
                element.className += (' ' + animationClass);

                clearTimeout(timeout);
                timeout = setTimeout(function() {
                    element.className = element.className.replace(/ is-(increasing|decreasing)/g, '');
                }, 3000);
            });
        });
    },

    /*
     * Credits to https://github.com/googleanalytics/ga-dev-tools
     * This is the Active Users component that polls
     * the number of active users on Analytics each 5 secs
     */
    analytics_create_components: function() {

        gapi.analytics.createComponent('ActiveUsers', {

            initialize: function() {
                this.activeUsers = 0;
                gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this));
            },

            execute: function() {
                // Stop any polling currently going on.
                if (this.polling_) {
                    this.stop();
                }

                this.render_();

                // Wait until the user is authorized.
                if (gapi.analytics.auth.isAuthorized()) {
                    this.pollActiveUsers_();
                } else {
                    gapi.analytics.auth.once('signIn', this.pollActiveUsers_.bind(this));
                }
            },

            stop: function() {
                clearTimeout(this.timeout_);
                this.polling_ = false;
                this.emit('stop', {activeUsers: this.activeUsers});
            },

            render_: function() {
                var opts = this.get();

                // Render the component inside the container.
                this.container = typeof opts.container === 'string' ?
                    document.getElementById(opts.container) : opts.container;

                this.container.innerHTML = opts.template || this.template;
                this.container.querySelector('b').innerHTML = this.activeUsers;
            },

            pollActiveUsers_: function() {
                var options = this.get();
                var pollingInterval = (options.pollingInterval || 5) * 1000;

                if (isNaN(pollingInterval) || pollingInterval < 5000) {
                    throw new Error('Frequency must be 5 seconds or more.');
                }

                this.polling_ = true;
                gapi.client.analytics.data.realtime
                    .get({ids:options.ids, metrics:'rt:activeUsers'})
                    .then(function(response) {
                        var result = response.result;
                        var newValue = result.totalResults ? +result.rows[0][0] : 0;
                        var oldValue = this.activeUsers;

                        this.emit('success', {activeUsers: this.activeUsers});

                        if (newValue !== oldValue) {
                            this.activeUsers = newValue;
                            this.onChange_(newValue - oldValue);
                        }

                        if (this.polling_) {
                            this.timeout_ = setTimeout(this.pollActiveUsers_.bind(this), pollingInterval);
                        }
                    }.bind(this));
            },

            onChange_: function(delta) {
                var valueContainer = this.container.querySelector('b');
                if (valueContainer) { valueContainer.innerHTML = this.activeUsers; }

                this.emit('change', {activeUsers: this.activeUsers, delta: delta});
                if (delta > 0) {
                    this.emit('increase', {activeUsers: this.activeUsers, delta: delta});
                } else {
                    this.emit('decrease', {activeUsers: this.activeUsers, delta: delta});
                }
            },

            handleSignOut_: function() {
                this.stop();
                gapi.analytics.auth.once('signIn', this.handleSignIn_.bind(this));
            },

            handleSignIn_: function() {
                this.pollActiveUsers_();
                gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this));
            },

            template:
                '<div class="ActiveUsers">' +
                    'Active Users: <b class="ActiveUsers-value"></b>' +
                '</div>'

        });
    },

    // Utility functions
    addLoader: function(selector) {
        var loader = '<span class="fa fa-3x fa-spin fa-spinner fa-pulse"/>';
        selector.html("<div class='o_loader'>" + loader + "</div>");
    },
    getDate: function(d) { return new Date(d[0]); },
    getValue: function(d) { return d[1]; },
    getPrunedTickValues: function(ticks, nb_desired_ticks) {
        var nb_values = ticks.length;
        var keep_one_of = Math.max(1, Math.floor(nb_values / nb_desired_ticks));

        return _.filter(ticks, function(d, i) {
            return i % keep_one_of === 0;
        });
    },
    format_number: function(value, type, digits, symbol) {
        if (type === 'currency') {
            return this.render_monetary_field(value, this.currency_id);
        } else {
            return field_utils.format[type](value || 0, {digits: digits}) + ' ' + symbol;
        }
    },
    render_monetary_field: function(value, currency_id) {
        var currency = session.get_currency(currency_id);
        var formatted_value = field_utils.format.float(value || 0, {digits: currency && currency.digits});
        if (currency) {
            if (currency.position === "after") {
                formatted_value += currency.symbol;
            } else {
                formatted_value = currency.symbol + formatted_value;
            }
        }
        return formatted_value;
    },

});

core.action_registry.add('backend_dashboard', Dashboard);

return Dashboard;
});