Beispiel #1
0
Datei: gui.js Projekt: 10537/odoo
odoo.define('point_of_sale.gui', function (require) {
"use strict";
// this file contains the Gui, which is the pos 'controller'. 
// It contains high level methods to manipulate the interface
// such as changing between screens, creating popups, etc.
//
// it is available to all pos objects trough the '.gui' field.

var core = require('web.core');
var field_utils = require('web.field_utils');
var session = require('web.session');

var _t = core._t;

var Gui = core.Class.extend({
    screen_classes: [],
    popup_classes:  [],
    init: function(options){
        var self = this;
        this.pos            = options.pos;
        this.chrome         = options.chrome;
        this.screen_instances     = {};
        this.popup_instances      = {};
        this.default_screen = null;
        this.startup_screen = null;
        this.current_popup  = null;
        this.current_screen = null; 

        this.chrome.ready.then(function(){
            self.close_other_tabs();
            var order = self.pos.get_order();
            if (order) {
                self.show_saved_screen(order);
            } else {
                self.show_screen(self.startup_screen);
            }
            self.pos.bind('change:selectedOrder', function(){
                self.show_saved_screen(self.pos.get_order());
            });
        });
    },

    /* ---- Gui: SCREEN MANIPULATION ---- */

    // register a screen widget to the gui,
    // it must have been inserted into the dom.
    add_screen: function(name, screen){
        screen.hide();
        this.screen_instances[name] = screen;
    },

    // sets the screen that will be displayed
    // for new orders
    set_default_screen: function(name){ 
        this.default_screen = name;
    },

    // sets the screen that will be displayed
    // when no orders are present
    set_startup_screen: function(name) {
        this.startup_screen = name;
    },

    // display the screen saved in an order,
    // called when the user changes the current order
    // no screen saved ? -> display default_screen
    // no order ? -> display startup_screen
    show_saved_screen:  function(order,options) {
        options = options || {};
        this.close_popup();
        if (order) {
            this.show_screen(order.get_screen_data('screen') || 
                             options.default_screen || 
                             this.default_screen,
                             null,'refresh');
        } else {
            this.show_screen(this.startup_screen);
        }
    },

    // display a screen. 
    // If there is an order, the screen will be saved in the order
    // - params: used to load a screen with parameters, for
    // example loading a 'product_details' screen for a specific product.
    // - refresh: if you want the screen to cycle trough show / hide even
    // if you are already on the same screen.
    show_screen: function(screen_name,params,refresh,skip_close_popup) {
        var screen = this.screen_instances[screen_name];
        if (!screen) {
            console.error("ERROR: show_screen("+screen_name+") : screen not found");
        }
        if (!skip_close_popup){
            this.close_popup();
        }
        var order = this.pos.get_order();
        if (order) {
            var old_screen_name = order.get_screen_data('screen');

            order.set_screen_data('screen',screen_name);

            if(params){
                order.set_screen_data('params',params);
            }

            if( screen_name !== old_screen_name ){
                order.set_screen_data('previous-screen',old_screen_name);
            }
        }

        if (refresh || screen !== this.current_screen) {
            if (this.current_screen) {
                this.current_screen.close();
                this.current_screen.hide();
            }
            this.current_screen = screen;
            this.current_screen.show(refresh);
        }
    },
    
    // returns the current screen.
    get_current_screen: function() {
        return this.pos.get_order() ? ( this.pos.get_order().get_screen_data('screen') || this.default_screen ) : this.startup_screen;
    },

    // goes to the previous screen (as specified in the order). The history only
    // goes 1 deep ...
    back: function() {
        var previous = this.pos.get_order().get_screen_data('previous-screen');
        if (previous) {
            this.show_screen(previous);
        }
    },

    // returns the parameter specified when this screen was displayed
    get_current_screen_param: function(param) {
        if (this.pos.get_order()) {
            var params = this.pos.get_order().get_screen_data('params');
            return params ? params[param] : undefined;
        } else {
            return undefined;
        }
    },

    /* ---- Gui: POPUP MANIPULATION ---- */

    // registers a new popup in the GUI.
    // the popup must have been previously inserted
    // into the dom.
    add_popup: function(name, popup) {
        popup.hide();
        this.popup_instances[name] = popup;
    },

    // displays a popup. Popup do not stack,
    // are not remembered by the order, and are
    // closed by screen changes or new popups.
    show_popup: function(name,options) {
        if (this.current_popup) {
            this.close_popup();
        }
        this.current_popup = this.popup_instances[name];
        return this.current_popup.show(options);
    },

    // close the current popup.
    close_popup: function() {
        if  (this.current_popup) {
            this.current_popup.close();
            this.current_popup.hide();
            this.current_popup = null;
        }
    },

    // is there an active popup ?
    has_popup: function() {
        return !!this.current_popup;
    },

    /* ---- Gui: INTER TAB COMM ---- */

    // This sets up automatic pos exit when open in
    // another tab.
    close_other_tabs: function() {
        var self = this;

        // avoid closing itself
        var now = Date.now();

        localStorage['message'] = '';
        localStorage['message'] = JSON.stringify({
            'message':'close_tabs',
            'session': this.pos.pos_session.id,
            'window_uid': now,
        });

        // storage events are (most of the time) triggered only when the
        // localstorage is updated in a different tab.
        // some browsers (e.g. IE) does trigger an event in the same tab
        // This may be a browser bug or a different interpretation of the HTML spec
        // cf https://connect.microsoft.com/IE/feedback/details/774798/localstorage-event-fired-in-source-window
        // Use window_uid parameter to exclude the current window
        window.addEventListener("storage", function(event) {
            var msg = event.data;

            if ( event.key === 'message' && event.newValue) {

                var msg = JSON.parse(event.newValue);
                if ( msg.message  === 'close_tabs' &&
                     msg.session  ==  self.pos.pos_session.id &&
                     msg.window_uid != now) {

                    console.info('POS / Session opened in another window. EXITING POS')
                    self._close();
                }
            }

        }, false);
    },

    /* ---- Gui: ACCESS CONTROL ---- */

    // A Generic UI that allow to select a user from a list.
    // It returns a deferred that resolves with the selected user 
    // upon success. Several options are available :
    // - security: passwords will be asked
    // - only_managers: restricts the list to managers
    // - current_user: password will not be asked if this 
    //                 user is selected.
    // - title: The title of the user selection list. 
    select_user: function(options){
        options = options || {};
        var self = this;
        var def  = new $.Deferred();

        var list = [];
        for (var i = 0; i < this.pos.users.length; i++) {
            var user = this.pos.users[i];
            if (!options.only_managers || user.role === 'manager') {
                list.push({
                    'label': user.name,
                    'item':  user,
                });
            }
        }

        this.show_popup('selection',{
            title: options.title || _t('Select User'),
            list: list,
            confirm: function(user){ def.resolve(user); },
            cancel: function(){ def.reject(); },
            is_selected: function(user){ return user === self.pos.get_cashier(); },
        });

        return def.then(function(user){
            if (options.security && user !== options.current_user && user.pos_security_pin) {
                return self.ask_password(user.pos_security_pin).then(function(){
                    return user;
                });
            } else {
                return user;
            }
        });
    },

    // Ask for a password, and checks if it this
    // the same as specified by the function call.
    // returns a deferred that resolves on success,
    // fails on failure.
    ask_password: function(password) {
        var self = this;
        var ret = new $.Deferred();
        if (password) {
            this.show_popup('password',{
                'title': _t('Password ?'),
                confirm: function(pw) {
                    if (pw !== password) {
                        self.show_popup('error',_t('Incorrect Password'));
                        ret.reject();
                    } else {
                        ret.resolve();
                    }
                },
            });
        } else {
            ret.resolve();
        }
        return ret;
    },

    // checks if the current user (or the user provided) has manager
    // access rights. If not, a popup is shown allowing the user to
    // temporarily login as an administrator. 
    // This method returns a deferred, that succeeds with the 
    // manager user when the login is successfull.
    sudo: function(user){
        user = user || this.pos.get_cashier();

        if (user.role === 'manager') {
            return new $.Deferred().resolve(user);
        } else {
            return this.select_user({
                security:       true, 
                only_managers:  true,
                title:       _t('Login as a Manager'),
            });
        }
    },

    /* ---- Gui: CLOSING THE POINT OF SALE ---- */

    close: function() {
        var self = this;
        var pending = this.pos.db.get_orders().length;

        if (!pending) {
            this._close();
        } else {
            this.pos.push_order().always(function() {
                var pending = self.pos.db.get_orders().length;
                if (!pending) {
                    self._close();
                } else {
                    var reason = self.pos.get('failed') ? 
                                 'configuration errors' : 
                                 'internet connection issues';  

                    self.show_popup('confirm', {
                        'title': _t('Offline Orders'),
                        'body':  _t(['Some orders could not be submitted to',
                                     'the server due to ' + reason + '.',
                                     'You can exit the Point of Sale, but do',
                                     'not close the session before the issue',
                                     'has been resolved.'].join(' ')),
                        'confirm': function() {
                            self._close();
                        },
                    });
                }
            });
        }
    },

    _close: function() {
        var self = this;
        this.chrome.loading_show();
        this.chrome.loading_message(_t('Closing ...'));

        this.pos.push_order().then(function(){
            var url = "/web#action=point_of_sale.action_client_pos_menu";
            window.location = session.debug ? $.param.querystring(url, {debug: session.debug}) : url;
        });
    },

    /* ---- Gui: SOUND ---- */

    play_sound: function(sound) {
        var src = '';
        if (sound === 'error') {
            src = "/point_of_sale/static/src/sounds/error.wav";
        } else if (sound === 'bell') {
            src = "/point_of_sale/static/src/sounds/bell.wav";
        } else {
            console.error('Unknown sound: ',sound);
            return;
        }
        $('body').append('<audio src="'+src+'" autoplay="true"></audio>');
    },

    /* ---- Gui: FILE I/O ---- */

    // This will make the browser download 'contents' as a 
    // file named 'name'
    // if 'contents' is not a string, it is converted into
    // a JSON representation of the contents. 


    // TODO: remove me in master: deprecated in favor of prepare_download_link
    // this method is kept for backward compatibility but is likely not going
    // to work as many browsers do to not accept fake click events on links
    download_file: function(contents, name) {
        href_params = this.prepare_file_blob(contents,name);
        var evt  = document.createEvent("HTMLEvents");
        evt.initEvent("click");

        $("<a>",href_params).get(0).dispatchEvent(evt);
        
    },      
    
    prepare_download_link: function(contents, filename, src, target) {
        var href_params = this.prepare_file_blob(contents, filename);

        $(target).parent().attr(href_params);
        $(src).addClass('oe_hidden');
        $(target).removeClass('oe_hidden');

        // hide again after click
        $(target).click(function() {
            $(src).removeClass('oe_hidden');
            $(this).addClass('oe_hidden');
        });
    },  
    
    prepare_file_blob: function(contents, name) {
        var URL = window.URL || window.webkitURL;
        
        if (typeof contents !== 'string') {
            contents = JSON.stringify(contents,null,2);
        }

        var blob = new Blob([contents]);

        return {download: name || 'document.txt',
                href: URL.createObjectURL(blob),}
    },

    /* ---- Gui: EMAILS ---- */

    // This will launch the user's email software
    // with a new email with the address, subject and body
    // prefilled.

    send_email: function(address, subject, body) {
        window.open("mailto:" + address + 
                          "?subject=" + (subject ? window.encodeURIComponent(subject) : '') +
                          "&body=" + (body ? window.encodeURIComponent(body) : ''));
    },

    /* ---- Gui: KEYBOARD INPUT ---- */

    // This is a helper to handle numpad keyboard input. 
    // - buffer: an empty or number string
    // - input:  '[0-9],'+','-','.','CLEAR','BACKSPACE'
    // - options: 'firstinput' -> will clear buffer if
    //     input is '[0-9]' or '.'
    //  returns the new buffer containing the modifications
    //  (the original is not touched) 
    numpad_input: function(buffer, input, options) { 
        var newbuf  = buffer.slice(0);
        options = options || {};
        var newbuf_float  = field_utils.parse.float(newbuf);
        var decimal_point = _t.database.parameters.decimal_point;

        if (input === decimal_point) {
            if (options.firstinput) {
                newbuf = "0.";
            }else if (!newbuf.length || newbuf === '-') {
                newbuf += "0.";
            } else if (newbuf.indexOf(decimal_point) < 0){
                newbuf = newbuf + decimal_point;
            }
        } else if (input === 'CLEAR') {
            newbuf = ""; 
        } else if (input === 'BACKSPACE') { 
            newbuf = newbuf.substring(0,newbuf.length - 1);
        } else if (input === '+') {
            if ( newbuf[0] === '-' ) {
                newbuf = newbuf.substring(1,newbuf.length);
            }
        } else if (input === '-') {
            if ( newbuf[0] === '-' ) {
                newbuf = newbuf.substring(1,newbuf.length);
            } else {
                newbuf = '-' + newbuf;
            }
        } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
            newbuf = this.chrome.format_currency_no_symbol(newbuf_float + parseFloat(input));
        } else if (!isNaN(parseInt(input))) {
            if (options.firstinput) {
                newbuf = '' + input;
            } else {
                newbuf += input;
            }
        }

        // End of input buffer at 12 characters.
        if (newbuf.length > buffer.length && newbuf.length > 12) {
            this.play_sound('bell');
            return buffer.slice(0);
        }

        return newbuf;
    },
});

var define_screen = function (classe) {
    Gui.prototype.screen_classes.push(classe);
};

var define_popup = function (classe) {
    Gui.prototype.popup_classes.push(classe);
};

return {
    Gui: Gui,
    define_screen: define_screen,
    define_popup: define_popup,
};

});
Beispiel #2
0
odoo.define('web.DataManager', function (require) {
"use strict";

var core = require('web.core');
var rpc = require('web.rpc');
var utils = require('web.utils');

return core.Class.extend({
    init: function () {
        this._init_cache();
        core.bus.on('clear_cache', this, this.invalidate.bind(this));
    },

    _init_cache: function () {
        this._cache = {
            actions: {},
            fields_views: {},
            filters: {},
            views: {},
        };
    },

    /**
     * Invalidates the whole cache
     * Suggestion: could be refined to invalidate some part of the cache
     */
    invalidate: function () {
        this._init_cache();
    },

    /**
     * Loads an action from its id or xmlid.
     *
     * @param {int|string} [action_id] the action id or xmlid
     * @param {Object} [additional_context] used to load the action
     * @return {Promise} resolved with the action whose id or xmlid is action_id
     */
    load_action: function (action_id, additional_context) {
        var self = this;
        var key = this._gen_key(action_id, additional_context || {});

        if (!this._cache.actions[key]) {
            this._cache.actions[key] = rpc.query({
                route: "/web/action/load",
                params: {
                    action_id: action_id,
                    additional_context : additional_context,
                },
            }).then(function (action) {
                self._cache.actions[key] = action.no_cache ? null : self._cache.actions[key];
                return action;
            }, this._invalidate.bind(this, this._cache.actions, key));
        }

        return this._cache.actions[key].then(function (action) {
            return $.extend(true, {}, action);
        });
    },

    /**
     * Loads various information concerning views: fields_view for each view,
     * the fields of the corresponding model, and optionally the filters.
     *
     * @param {Object} params
     * @param {String} params.model
     * @param {Object} params.context
     * @param {Array} params.views_descr array of [view_id, view_type]
     * @param {Object} [options] dictionary of various options:
     *     - options.load_filters: whether or not to load the filters,
     *     - options.action_id: the action_id (required to load filters),
     *     - options.toolbar: whether or not a toolbar will be displayed,
     * @return {Promise} resolved with the requested views information
     */
    load_views: function (params, options) {
        var self = this;

        var model = params.model;
        var context = params.context;
        var views_descr = params.views_descr;
        var key = this._gen_key(model, views_descr, options || {}, context);

        if (!this._cache.views[key]) {
            // Don't load filters if already in cache
            var filters_key;
            if (options.load_filters) {
                filters_key = this._gen_key(model, options.action_id);
                options.load_filters = !this._cache.filters[filters_key];
            }

            this._cache.views[key] = rpc.query({
                args: [],
                kwargs: {
                    views: views_descr,
                    options: options,
                    context: context,
                },
                model: model,
                method: 'load_views',
            }).then(function (result) {
                // Freeze the fields dict as it will be shared between views and
                // no one should edit it
                utils.deepFreeze(result.fields);

                // Insert views into the fields_views cache
                _.each(views_descr, function (view_descr) {
                    var toolbar = options.toolbar && view_descr[1] !== 'search';
                    var fv_key = self._gen_key(model, view_descr[0], view_descr[1], toolbar, context);
                    var fvg = result.fields_views[view_descr[1]];
                    fvg.viewFields = fvg.fields;
                    fvg.fields = result.fields;
                    self._cache.fields_views[fv_key] = Promise.resolve(fvg);
                });

                // Insert filters, if any, into the filters cache
                if (result.filters) {
                    self._cache.filters[filters_key] = Promise.resolve(result.filters);
                }

                return result.fields_views;
            }, this._invalidate.bind(this, this._cache.views, key));
        }

        return this._cache.views[key];
    },

    /**
     * Loads the filters of a given model and optional action id.
     *
     * @param {Object} params
     * @param {string} params.modelName
     * @param {Object} params.context
     * @param {integer} params.actionId
     * @return {Promise} resolved with the requested filters
     */
    load_filters: function (params) {
        var key = this._gen_key(params.modelName, params.actionId);
        if (!this._cache.filters[key]) {
            this._cache.filters[key] = rpc.query({
                args: [params.modelName, params.actionId],
                kwargs: {
                    context: params.context || {},
                    // get_context() de dataset
                },
                model: 'ir.filters',
                method: 'get_filters',
            }).guardedCatch(this._invalidate.bind(this, this._cache.filters, key));
        }
        return this._cache.filters[key];
    },

    /**
     * Calls 'create_or_replace' on 'ir_filters'.
     *
     * @param {Object} [filter] the filter description
     * @return {Promise} resolved with the id of the created or replaced filter
     */
    create_filter: function (filter) {
        var self = this;
        return rpc.query({
                args: [filter],
                model: 'ir.filters',
                method: 'create_or_replace',
            })
            .then(function (filterId) {
                var key = [
                    filter.model_id,
                    filter.action_id || false,
                ].join(',');
                self._invalidate(self._cache.filters, key);
                return filterId;
            });
    },

    /**
     * Calls 'unlink' on 'ir_filters'.
     *
     * @param {integer} filterId Id of the filter to remove
     * @return {Promise}
     */
    delete_filter: function (filterId) {
        var self = this;
        return rpc.query({
                args: [filterId],
                model: 'ir.filters',
                method: 'unlink',
            })
            .then(function () {
                self._cache.filters = {}; // invalidate cache
            });
    },

    /**
     * Private function that generates a cache key from its arguments
     */
    _gen_key: function () {
        return _.map(Array.prototype.slice.call(arguments), function (arg) {
            if (!arg) {
                return false;
            }
            return _.isObject(arg) ? JSON.stringify(arg) : arg;
        }).join(',');
    },

    /**
     * Private function that invalidates a cache entry
     */
    _invalidate: function (cache, key) {
        delete cache[key];
    },
});

});
Beispiel #3
0
ecore.define('point_of_sale.DB', function (require) {
"use strict";

var core = require('web.core');
/* The PosDB holds reference to data that is either
 * - static: does not change between pos reloads
 * - persistent : must stay between reloads ( orders )
 */

var PosDB = core.Class.extend({
    name: 'ecore_pos_db', //the prefix of the localstorage data
    limit: 100,  // the maximum number of results returned by a search
    init: function(options){
        options = options || {};
        this.name = options.name || this.name;
        this.limit = options.limit || this.limit;
        
        if (options.uuid) {
            this.name = this.name + '_' + options.uuid;
        }

        //cache the data in memory to avoid roundtrips to the localstorage
        this.cache = {};

        this.product_by_id = {};
        this.product_by_barcode = {};
        this.product_by_category_id = {};

        this.partner_sorted = [];
        this.partner_by_id = {};
        this.partner_by_barcode = {};
        this.partner_search_string = "";
        this.partner_write_date = null;

        this.category_by_id = {};
        this.root_category_id  = 0;
        this.category_products = {};
        this.category_ancestors = {};
        this.category_childs = {};
        this.category_parent    = {};
        this.category_search_string = {};
        this.packagings_by_id = {};
        this.packagings_by_product_tmpl_id = {};
        this.packagings_by_barcode = {};
    },

    /* 
     * sets an uuid to prevent conflict in locally stored data between multiple databases running
     * in the same browser at the same origin (Doing this is not advised !)
     */
    set_uuid: function(uuid){
        this.name = this.name + '_' + uuid;
    },

    /* returns the category object from its id. If you pass a list of id as parameters, you get
     * a list of category objects. 
     */  
    get_category_by_id: function(categ_id){
        if(categ_id instanceof Array){
            var list = [];
            for(var i = 0, len = categ_id.length; i < len; i++){
                var cat = this.category_by_id[categ_id[i]];
                if(cat){
                    list.push(cat);
                }else{
                    console.error("get_category_by_id: no category has id:",categ_id[i]);
                }
            }
            return list;
        }else{
            return this.category_by_id[categ_id];
        }
    },
    /* returns a list of the category's child categories ids, or an empty list 
     * if a category has no childs */
    get_category_childs_ids: function(categ_id){
        return this.category_childs[categ_id] || [];
    },
    /* returns a list of all ancestors (parent, grand-parent, etc) categories ids
     * starting from the root category to the direct parent */
    get_category_ancestors_ids: function(categ_id){
        return this.category_ancestors[categ_id] || [];
    },
    /* returns the parent category's id of a category, or the root_category_id if no parent.
     * the root category is parent of itself. */
    get_category_parent_id: function(categ_id){
        return this.category_parent[categ_id] || this.root_category_id;
    },
    /* adds categories definitions to the database. categories is a list of categories objects as
     * returned by the ecore server. Categories must be inserted before the products or the 
     * product/ categories association may (will) not work properly */
    add_categories: function(categories){
        var self = this;
        if(!this.category_by_id[this.root_category_id]){
            this.category_by_id[this.root_category_id] = {
                id : this.root_category_id,
                name : 'Root',
            };
        }
        for(var i=0, len = categories.length; i < len; i++){
            this.category_by_id[categories[i].id] = categories[i];
        }
        len = categories.length;
        for(i=0; i < len; i++){
            var cat = categories[i];
            var parent_id = cat.parent_id[0] || this.root_category_id;
            this.category_parent[cat.id] = cat.parent_id[0];
            if(!this.category_childs[parent_id]){
                this.category_childs[parent_id] = [];
            }
            this.category_childs[parent_id].push(cat.id);
        }
        function make_ancestors(cat_id, ancestors){
            self.category_ancestors[cat_id] = ancestors;

            ancestors = ancestors.slice(0);
            ancestors.push(cat_id);

            var childs = self.category_childs[cat_id] || [];
            for(var i=0, len = childs.length; i < len; i++){
                make_ancestors(childs[i], ancestors);
            }
        }
        make_ancestors(this.root_category_id, []);
    },
    category_contains: function(categ_id, product_id) {
        var product = this.product_by_id[product_id];
        if (product) {
            var cid = product.pos_categ_id[0];
            while (cid && cid !== categ_id){
                cid = this.category_parent[cid];
            }
            return !!cid;
        }
        return false;
    },
    /* loads a record store from the database. returns default if nothing is found */
    load: function(store,deft){
        if(this.cache[store] !== undefined){
            return this.cache[store];
        }
        var data = localStorage[this.name + '_' + store];
        if(data !== undefined && data !== ""){
            data = JSON.parse(data);
            this.cache[store] = data;
            return data;
        }else{
            return deft;
        }
    },
    /* saves a record store to the database */
    save: function(store,data){
        localStorage[this.name + '_' + store] = JSON.stringify(data);
        this.cache[store] = data;
    },
    _product_search_string: function(product){
        var str = product.display_name;
        if (product.barcode) {
            str += '|' + product.barcode;
        }
        if (product.default_code) {
            str += '|' + product.default_code;
        }
        if (product.description) {
            str += '|' + product.description;
        }
        if (product.description_sale) {
            str += '|' + product.description_sale;
        }
        var packagings = this.packagings_by_product_tmpl_id[product.product_tmpl_id] || [];
        for (var i = 0; i < packagings.length; i++) {
            str += '|' + packagings[i].barcode;
        }
        str  = product.id + ':' + str.replace(/:/g,'') + '\n';
        return str;
    },
    add_products: function(products){
        var stored_categories = this.product_by_category_id;

        if(!products instanceof Array){
            products = [products];
        }
        for(var i = 0, len = products.length; i < len; i++){
            var product = products[i];
            var search_string = this._product_search_string(product);
            var categ_id = product.pos_categ_id ? product.pos_categ_id[0] : this.root_category_id;
            product.product_tmpl_id = product.product_tmpl_id[0];
            if(!stored_categories[categ_id]){
                stored_categories[categ_id] = [];
            }
            stored_categories[categ_id].push(product.id);

            if(this.category_search_string[categ_id] === undefined){
                this.category_search_string[categ_id] = '';
            }
            this.category_search_string[categ_id] += search_string;

            var ancestors = this.get_category_ancestors_ids(categ_id) || [];

            for(var j = 0, jlen = ancestors.length; j < jlen; j++){
                var ancestor = ancestors[j];
                if(! stored_categories[ancestor]){
                    stored_categories[ancestor] = [];
                }
                stored_categories[ancestor].push(product.id);

                if( this.category_search_string[ancestor] === undefined){
                    this.category_search_string[ancestor] = '';
                }
                this.category_search_string[ancestor] += search_string; 
            }
            this.product_by_id[product.id] = product;
            if(product.barcode){
                this.product_by_barcode[product.barcode] = product;
            }
        }
    },
    add_packagings: function(packagings){
        for(var i = 0, len = packagings.length; i < len; i++){
            var pack = packagings[i];
            this.packagings_by_id[pack.id] = pack;
            if(!this.packagings_by_product_tmpl_id[pack.product_tmpl_id[0]]){
                this.packagings_by_product_tmpl_id[pack.product_tmpl_id[0]] = [];
            }
            this.packagings_by_product_tmpl_id[pack.product_tmpl_id[0]].push(pack);
            if(pack.barcode){
                this.packagings_by_barcode[pack.barcode] = pack;
            }
        }
    },
    _partner_search_string: function(partner){
        var str =  partner.name;
        if(partner.barcode){
            str += '|' + partner.barcode;
        }
        if(partner.address){
            str += '|' + partner.address;
        }
        if(partner.phone){
            str += '|' + partner.phone.split(' ').join('');
        }
        if(partner.mobile){
            str += '|' + partner.mobile.split(' ').join('');
        }
        if(partner.email){
            str += '|' + partner.email;
        }
        str = '' + partner.id + ':' + str.replace(':','') + '\n';
        return str;
    },
    add_partners: function(partners){
        var updated_count = 0;
        var new_write_date = '';
        var partner;
        for(var i = 0, len = partners.length; i < len; i++){
            partner = partners[i];

            if (    this.partner_write_date && 
                    this.partner_by_id[partner.id] &&
                    new Date(this.partner_write_date).getTime() + 1000 >=
                    new Date(partner.write_date).getTime() ) {
                // FIXME: The write_date is stored with milisec precision in the database
                // but the dates we get back are only precise to the second. This means when
                // you read partners modified strictly after time X, you get back partners that were
                // modified X - 1 sec ago. 
                continue;
            } else if ( new_write_date < partner.write_date ) { 
                new_write_date  = partner.write_date;
            }
            if (!this.partner_by_id[partner.id]) {
                this.partner_sorted.push(partner.id);
            }
            this.partner_by_id[partner.id] = partner;

            updated_count += 1;
        }

        this.partner_write_date = new_write_date || this.partner_write_date;

        if (updated_count) {
            // If there were updates, we need to completely 
            // rebuild the search string and the barcode indexing

            this.partner_search_string = "";
            this.partner_by_barcode = {};

            for (var id in this.partner_by_id) {
                partner = this.partner_by_id[id];

                if(partner.barcode){
                    this.partner_by_barcode[partner.barcode] = partner;
                }
                partner.address = (partner.street || '') +', '+ 
                                  (partner.zip || '')    +' '+
                                  (partner.city || '')   +', '+ 
                                  (partner.country_id[1] || '');
                this.partner_search_string += this._partner_search_string(partner);
            }
        }
        return updated_count;
    },
    get_partner_write_date: function(){
        return this.partner_write_date || "1970-01-01 00:00:00";
    },
    get_partner_by_id: function(id){
        return this.partner_by_id[id];
    },
    get_partner_by_barcode: function(barcode){
        return this.partner_by_barcode[barcode];
    },
    get_partners_sorted: function(max_count){
        max_count = max_count ? Math.min(this.partner_sorted.length, max_count) : this.partner_sorted.length;
        var partners = [];
        for (var i = 0; i < max_count; i++) {
            partners.push(this.partner_by_id[this.partner_sorted[i]]);
        }
        return partners;
    },
    search_partner: function(query){
        try {
            query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.');
            query = query.replace(' ','.+');
            var re = RegExp("([0-9]+):.*?"+query,"gi");
        }catch(e){
            return [];
        }
        var results = [];
        for(var i = 0; i < this.limit; i++){
            var r = re.exec(this.partner_search_string);
            if(r){
                var id = Number(r[1]);
                results.push(this.get_partner_by_id(id));
            }else{
                break;
            }
        }
        return results;
    },
    /* removes all the data from the database. TODO : being able to selectively remove data */
    clear: function(){
        for(var i = 0, len = arguments.length; i < len; i++){
            localStorage.removeItem(this.name + '_' + arguments[i]);
        }
    },
    /* this internal methods returns the count of properties in an object. */
    _count_props : function(obj){
        var count = 0;
        for(var prop in obj){
            if(obj.hasOwnProperty(prop)){
                count++;
            }
        }
        return count;
    },
    get_product_by_id: function(id){
        return this.product_by_id[id];
    },
    get_product_by_barcode: function(barcode){
        if(this.product_by_barcode[barcode]){
            return this.product_by_barcode[barcode];
        }
        var pack = this.packagings_by_barcode[barcode];
        if(pack){
            return this.product_by_id[pack.product_tmpl_id[0]];
        }
        return undefined;
    },
    get_product_by_category: function(category_id){
        var product_ids  = this.product_by_category_id[category_id];
        var list = [];
        if (product_ids) {
            for (var i = 0, len = Math.min(product_ids.length, this.limit); i < len; i++) {
                list.push(this.product_by_id[product_ids[i]]);
            }
        }
        return list;
    },
    /* returns a list of products with :
     * - a category that is or is a child of category_id,
     * - a name, package or barcode containing the query (case insensitive) 
     */
    search_product_in_category: function(category_id, query){
        try {
            query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.');
            query = query.replace(/ /g,'.+');
            var re = RegExp("([0-9]+):.*?"+query,"gi");
        }catch(e){
            return [];
        }
        var results = [];
        for(var i = 0; i < this.limit; i++){
            var r = re.exec(this.category_search_string[category_id]);
            if(r){
                var id = Number(r[1]);
                results.push(this.get_product_by_id(id));
            }else{
                break;
            }
        }
        return results;
    },
    /* from a product id, and a list of category ids, returns
     * true if the product belongs to one of the provided category
     * or one of its child categories.
     */
    is_product_in_category: function(category_ids, product_id) {
        if (!(category_ids instanceof Array)) {
            category_ids = [category_ids];
        }
        var cat = this.get_product_by_id(product_id).pos_categ_id[0];
        while (cat) {
            for (var i = 0; i < category_ids.length; i++) {
                if (cat == category_ids[i]) {   // The == is important, ids may be strings
                    return true;
                }
            }
            cat = this.get_category_parent_id(cat);
        }
        return false;
    },

    /* paid orders */
    add_order: function(order){
        var order_id = order.uid;
        var orders  = this.load('orders',[]);

        // if the order was already stored, we overwrite its data
        for(var i = 0, len = orders.length; i < len; i++){
            if(orders[i].id === order_id){
                orders[i].data = order;
                this.save('orders',orders);
                return order_id;
            }
        }

        orders.push({id: order_id, data: order});
        this.save('orders',orders);
        return order_id;
    },
    remove_order: function(order_id){
        var orders = this.load('orders',[]);
        orders = _.filter(orders, function(order){
            return order.id !== order_id;
        });
        this.save('orders',orders);
    },
    remove_all_orders: function(){
        this.save('orders',[]);
    },
    get_orders: function(){
        return this.load('orders',[]);
    },
    get_order: function(order_id){
        var orders = this.get_orders();
        for(var i = 0, len = orders.length; i < len; i++){
            if(orders[i].id === order_id){
                return orders[i];
            }
        }
        return undefined;
    },

    /* working orders */
    save_unpaid_order: function(order){
        var order_id = order.uid;
        var orders = this.load('unpaid_orders',[]);
        var serialized = order.export_as_JSON();

        for (var i = 0; i < orders.length; i++) {
            if (orders[i].id === order_id){
                orders[i].data = serialized;
                this.save('unpaid_orders',orders);
                return order_id;
            }
        }

        orders.push({id: order_id, data: serialized});
        this.save('unpaid_orders',orders);
        return order_id;
    },
    remove_unpaid_order: function(order){
        var orders = this.load('unpaid_orders',[]);
        orders = _.filter(orders, function(o){
            return o.id !== order.uid;
        });
        this.save('unpaid_orders',orders);
    },
    remove_all_unpaid_orders: function(){
        this.save('unpaid_orders',[]);
    },
    get_unpaid_orders: function(){
        var saved = this.load('unpaid_orders',[]);
        var orders = [];
        for (var i = 0; i < saved.length; i++) {
            orders.push(saved[i].data);
        }
        return orders;
    },
});

return PosDB;

});
Beispiel #4
0
ecore.define('web.form_common', function (require) {
"use strict";

var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
var ListView = require('web.ListView');
var pyeval = require('web.pyeval');
var SearchView = require('web.SearchView');
var session = require('web.session');
var utils = require('web.utils');
var Widget = require('web.Widget');

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

/**
 * Interface implemented by the form view or any other object
 * able to provide the features necessary for the fields to work.
 *
 * Properties:
 *     - display_invalid_fields : if true, all fields where is_valid() return true should
 *     be displayed as invalid.
 *     - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create".
 * Events:
 *     - view_content_has_changed : when the values of the fields have changed. When
 *     this event is triggered all fields should reprocess their modifiers.
 *     - field_changed:<field_name> : when the value of a field change, an event is triggered
 *     named "field_changed:<field_name>" with <field_name> replaced by the name of the field.
 *     This event is not related to the on_change mechanism of eCore and is always called
 *     when the value of a field is setted or changed. This event is only triggered when the
 *     value of the field is syntactically valid, but it can be triggered when the value
 *     is sematically invalid (ie, when a required field is false). It is possible that an event
 *     about a precise field is never triggered even if that field exists in the view, in that
 *     case the value of the field is assumed to be false.
 */
var FieldManagerMixin = {
    /**
     * Must return the asked field as in fields_get.
     */
    get_field_desc: function(field_name) {},
    /**
     * Returns the current value of a field present in the view. See the get_value() method
     * method in FieldInterface for further information.
     */
    get_field_value: function(field_name) {},
    /**
    Gives new values for the fields contained in the view. The new values could not be setted
    right after the call to this method. Setting new values can trigger on_changes.

    @param {Object} values A dictonary with key = field name and value = new value.
    @return {$.Deferred} Is resolved after all the values are setted.
    */
    set_values: function(values) {},
    /**
    Computes an eCore domain.

    @param {Array} expression An eCore domain.
    @return {boolean} The computed value of the domain.
    */
    compute_domain: function(expression) {},
    /**
    Builds an evaluation context for the resolution of the fields' contexts. Please note
    the field are only supposed to use this context to evualuate their own, they should not
    extend it.

    @return {CompoundContext} An eCore context.
    */
    build_eval_context: function() {},
};

/**
    Welcome.

    If you read this documentation, it probably means that you were asked to use a form view widget outside of
    a form view. Before going further, you must understand that those fields were never really created for
    that usage. Don't think that this class will hold the answer to all your problems, at best it will allow
    you to hack the system with more style.
*/
var DefaultFieldManager = Widget.extend({
    init: function(parent, eval_context) {
        this._super(parent);
        this.field_descs = {};
        this.eval_context = eval_context || {};
        this.set({
            display_invalid_fields: false,
            actual_mode: 'create',
        });
    },
    get_field_desc: function(field_name) {
        if (this.field_descs[field_name] === undefined) {
            this.field_descs[field_name] = {
                string: field_name,
            };
        }
        return this.field_descs[field_name];
    },
    extend_field_desc: function(fields) {
        var self = this;
        _.each(fields, function(v, k) {
            _.extend(self.get_field_desc(k), v);
        });
    },
    get_field_value: function(field_name) {
        return false;
    },
    set_values: function(values) {
        // nothing
    },
    compute_domain: function(expression) {
        return data.compute_domain(expression, {});
    },
    build_eval_context: function() {
        return new data.CompoundContext(this.eval_context);
    },
});

/**
 * A mixin to apply on any FormWidget that has to completely re-render when its readonly state
 * switch.
 */
var ReinitializeWidgetMixin =  {
    /**
     * Default implementation of, you should not override it, use initialize_field() instead.
     */
    start: function() {
        this.initialize_field();
        this._super();
    },
    initialize_field: function() {
        this.on("change:effective_readonly", this, this.reinitialize);
        this.initialize_content();
    },
    reinitialize: function() {
        this.destroy_content();
        this.renderElement();
        this.initialize_content();
    },
    /**
     * Called to destroy anything that could have been created previously, called before a
     * re-initialization.
     */
    destroy_content: function() {},
    /**
     * Called to initialize the content.
     */
    initialize_content: function() {},
};

/**
 * A mixin to apply on any field that has to completely re-render when its readonly state
 * switch.
 */
var ReinitializeFieldMixin =  _.extend({}, ReinitializeWidgetMixin, {
    reinitialize: function() {
        ReinitializeWidgetMixin.reinitialize.call(this);
        var res = this.render_value();
        if (this.view && this.view.render_value_defs){
            this.view.render_value_defs.push(res);
        }
    },
});

/**
    A mixin containing some useful methods to handle completion inputs.
    
    The widget containing this option can have these arguments in its widget options:
    - no_quick_create: if true, it will disable the quick create
*/
var CompletionFieldMixin = {
    init: function() {
        this.limit = 7;
        this.orderer = new utils.DropMisordered();
        this.can_create = this.node.attrs.can_create || true;
        this.can_write = this.node.attrs.can_write || true;
    },
    /**
     * Call this method to search using a string.
     */
    get_search_result: function(search_val) {
        var self = this;

        var dataset = new data.DataSet(this, this.field.relation, self.build_context());
        this.last_query = search_val;
        var exclusion_domain = [], ids_blacklist = this.get_search_blacklist();
        if (!_(ids_blacklist).isEmpty()) {
            exclusion_domain.push(['id', 'not in', ids_blacklist]);
        }

        return this.orderer.add(dataset.name_search(
                search_val, new data.CompoundDomain(self.build_domain(), exclusion_domain),
                'ilike', this.limit + 1, self.build_context())).then(function(data) {
            self.last_search = data;
            // possible selections for the m2o
            var values = _.map(data, function(x) {
                x[1] = x[1].split("\n")[0];
                return {
                    label: _.str.escapeHTML(x[1]),
                    value: x[1],
                    name: x[1],
                    id: x[0],
                };
            });

            // search more... if more results that max
            if (values.length > self.limit) {
                values = values.slice(0, self.limit);
                values.push({
                    label: _t("Search More..."),
                    action: function() {
                        dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) {
                            self._search_create_popup("search", data);
                        });
                    },
                    classname: 'o_m2o_dropdown_option'
                });
            }
            // quick create
            var raw_result = _(data.result).map(function(x) {return x[1];});
            if (search_val.length > 0 && !_.include(raw_result, search_val) &&
                ! (self.options && (self.options.no_create || self.options.no_quick_create))) {
                self.can_create && values.push({
                    label: _.str.sprintf(_t('Create "<strong>%s</strong>"'),
                        $('<span />').text(search_val).html()),
                    action: function() {
                        self._quick_create(search_val);
                    },
                    classname: 'o_m2o_dropdown_option'
                });
            }
            // create...
            if (!(self.options && (self.options.no_create || self.options.no_create_edit)) && self.can_create){
                values.push({
                    label: _t("Create and Edit..."),
                    action: function() {
                        self._search_create_popup("form", undefined, self._create_context(search_val));
                    },
                    classname: 'o_m2o_dropdown_option'
                });
            }
            else if (values.length === 0) {
                values.push({
                    label: _t("No results to show..."),
                    action: function() {},
                    classname: 'o_m2o_dropdown_option'
                });
            }

            return values;
        });
    },
    get_search_blacklist: function() {
        return [];
    },
    _quick_create: function(name) {
        var self = this;
        var slow_create = function () {
            self._search_create_popup("form", undefined, self._create_context(name));
        };
        if (self.options.quick_create === undefined || self.options.quick_create) {
            new data.DataSet(this, this.field.relation, self.build_context())
                .name_create(name).done(function(data) {
                    if (!self.get('effective_readonly'))
                        self.add_id(data[0]);
                }).fail(function(error, event) {
                    event.preventDefault();
                    slow_create();
                });
        } else
            slow_create();
    },
    // all search/create popup handling
    _search_create_popup: function(view, ids, context) {
        var self = this;
        new SelectCreateDialog(this, _.extend({}, (this.options || {}), {
            res_model: self.field.relation,
            domain: self.build_domain(),
            context: new data.CompoundContext(self.build_context(), context || {}),
            title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
            initial_ids: ids ? _.map(ids, function(x) {return x[0];}) : undefined,
            initial_view: view,
            disable_multiple_selection: true,
            on_selected: function(element_ids) {
                self.add_id(element_ids[0]);
                self.focus();
            }
        })).open();
    },
    /**
     * To implement.
     */
    add_id: function(id) {},
    _create_context: function(name) {
        var tmp = {};
        var field = (this.options || {}).create_name_field;
        if (field === undefined)
            field = "name";
        if (field !== false && name && (this.options || {}).quick_create !== false)
            tmp["default_" + field] = name;
        return tmp;
    },
};

/**
 * Must be applied over an class already possessing the PropertiesMixin.
 *
 * Apply the result of the "invisible" domain to this.$el.
 */
var InvisibilityChangerMixin = {
    init: function(field_manager, invisible_domain) {
        var self = this;
        this._ic_field_manager = field_manager;
        this._ic_invisible_modifier = invisible_domain;
        this._ic_field_manager.on("view_content_has_changed", this, function() {
            var result = self._ic_invisible_modifier === undefined ? false :
                self._ic_field_manager.compute_domain(self._ic_invisible_modifier);
            self.set({"invisible": result});
        });
        this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false});
        var check = function() {
            if (self.get("invisible") || self.get('force_invisible')) {
                self.set({"effective_invisible": true});
            } else {
                self.set({"effective_invisible": false});
            }
        };
        this.on('change:invisible', this, check);
        this.on('change:force_invisible', this, check);
        check.call(this);
    },
    start: function() {
        this.on("change:effective_invisible", this, this._check_visibility);
        this._check_visibility();
    },
    _check_visibility: function() {
        this.$el.toggleClass('o_form_invisible', this.get("effective_invisible"));
    },
};

var InvisibilityChanger = core.Class.extend(core.mixins.PropertiesMixin, InvisibilityChangerMixin, {
    init: function(parent, field_manager, invisible_domain, $el) {
        this.setParent(parent);
        core.mixins.PropertiesMixin.init.call(this);
        InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain);
        this.$el = $el;
        this.start();
    },
});

// Specialization of InvisibilityChanger for the `notebook` form element to handle more
// elegantly its special cases (i.e. activate the closest visible sibling)
var NotebookInvisibilityChanger = InvisibilityChanger.extend({
    // Override start so that it does not call _check_visibility since it will be
    // called again when view content will be loaded (event view_content_has_changed)
    start: function() {
        this.on("change:effective_invisible", this, this._check_visibility);
    },
    _check_visibility: function() {
        this._super();
        if (this.get("effective_invisible") === true) {
            // Switch to invisible
            // Remove this element as active and set a visible sibling active (if there is one)
            if (this.$el.hasClass('active')) {
                this.$el.removeClass('active');
                var visible_siblings = this.$el.siblings(':not(.o_form_invisible)');
                if (visible_siblings.length) {
                    $(visible_siblings[0]).addClass('active');
                }
            }
        } else {
            // Switch to visible
            // If there is no visible active sibling, set this element as active,
            // otherwise if that sibling hasn't autofocus and if we are in edit mode,
            //    remove that sibling as active and set this element as active
            var visible_active_sibling = this.$el.siblings(':not(.o_form_invisible).active');
            if (!(visible_active_sibling.length)) {
                this.$el.addClass('active');
            } else if (!$(visible_active_sibling[0]).data('autofocus') && this._ic_field_manager.get('actual_mode') === "edit") {
                this.$el.addClass('active');
                $(visible_active_sibling[0]).removeClass('active');
            }
        }
    },
});

/**
    Base class for all fields, custom widgets and buttons to be displayed in the form view.

    Properties:
        - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending
        the values of the "readonly" property and the "mode" property on the field manager.
*/
var FormWidget = Widget.extend(InvisibilityChangerMixin, {
    /**
     * @constructs instance.web.form.FormWidget
     * @extends instance.web.Widget
     *
     * @param field_manager
     * @param node
     */
    init: function(field_manager, node) {
        this._super(field_manager);
        this.field_manager = field_manager;
        this.view = this.field_manager;
        this.node = node;
        this.session = session;
        this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}');
        InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible);

        this.field_manager.on("view_content_has_changed", this, this.process_modifiers);

        this.set({
            required: false,
            readonly: false,
        });
        // some events to make the property "effective_readonly" sync automatically with "readonly" and
        // "mode" on field_manager
        var self = this;
        var test_effective_readonly = function() {
            self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"});
        };
        this.on("change:readonly", this, test_effective_readonly);
        this.field_manager.on("change:actual_mode", this, test_effective_readonly);
        test_effective_readonly.call(this);
    },
    renderElement: function() {
        this.process_modifiers();
        this._super();
        this.$el.addClass(this.node.attrs["class"] || "");
    },
    destroy: function() {
        $.fn.tooltip('destroy');
        this._super.apply(this, arguments);
    },
    /**
     * Sets up blur/focus forwarding from DOM elements to a widget (`this`).
     *
     * This method is an utility method that is meant to be called by child classes.
     *
     * @param {jQuery} $e jQuery object of elements to bind focus/blur on
     */
    setupFocus: function ($e) {
        var self = this;
        $e.on({
            focus: function () { self.trigger('focused'); },
            blur: function () { self.trigger('blurred'); }
        });
    },
    process_modifiers: function() {
        var to_set = {};
        for (var a in this.modifiers) {
            if (!this.modifiers.hasOwnProperty(a)) { continue; }
            if (!_.include(["invisible"], a)) {
                var val = this.field_manager.compute_domain(this.modifiers[a]);
                to_set[a] = val;
            }
        }
        this.set(to_set);
    },
    do_attach_tooltip: function(widget, trigger, options) {
        widget = widget || this;
        trigger = trigger || this.$el;
        options = _.extend({
                delay: { show: 1000, hide: 0 },
                title: function() {
                    var template = widget.template + '.tooltip';
                    if (!QWeb.has_template(template)) {
                        template = 'WidgetLabel.tooltip';
                    }
                    return QWeb.render(template, {
                        debug: session.debug,
                        widget: widget
                    });
                }
            }, options || {});
        //only show tooltip if we are in debug or if we have a help to show, otherwise it will display
        //as empty
        if (session.debug || widget.node.attrs.help || (widget.field && widget.field.help)){
            $(trigger).tooltip(options);
        }
    },
    /**
     * Builds a new context usable for operations related to fields by merging
     * the fields'context with the action's context.
     */
    build_context: function() {
        // only use the model's context if there is not context on the node
        var v_context = this.node.attrs.context;
        if (! v_context) {
            v_context = (this.field || {}).context || {};
        }

        if (v_context.__ref || true) { //TODO: remove true
            var fields_values = this.field_manager.build_eval_context();
            v_context = new data.CompoundContext(v_context).set_eval_context(fields_values);
        }
        return v_context;
    },
    build_domain: function() {
        var f_domain = this.field.domain || [];
        var n_domain = this.node.attrs.domain || null;
        // if there is a domain on the node, overrides the model's domain
        var final_domain = n_domain !== null ? n_domain : f_domain;
        if (!(final_domain instanceof Array) || true) { //TODO: remove true
            var fields_values = this.field_manager.build_eval_context();
            final_domain = new data.CompoundDomain(final_domain).set_eval_context(fields_values);
        }
        return final_domain;
    }
});

/*
# Values: (0, 0,  { fields })    create
#         (1, ID, { fields })    update
#         (2, ID)                remove (delete)
#         (3, ID)                unlink one (target id or target of relation)
#         (4, ID)                link
#         (5)                    unlink all (only valid for one2many)
*/
var commands = {
    // (0, _, {values})
    CREATE: 0,
    'create': function (values) {
        return [commands.CREATE, false, values];
    },
    // (1, id, {values})
    UPDATE: 1,
    'update': function (id, values) {
        return [commands.UPDATE, id, values];
    },
    // (2, id[, _])
    DELETE: 2,
    'delete': function (id) {
        return [commands.DELETE, id, false];
    },
    // (3, id[, _]) removes relation, but not linked record itself
    FORGET: 3,
    'forget': function (id) {
        return [commands.FORGET, id, false];
    },
    // (4, id[, _])
    LINK_TO: 4,
    'link_to': function (id) {
        return [commands.LINK_TO, id, false];
    },
    // (5[, _[, _]])
    DELETE_ALL: 5,
    'delete_all': function () {
        return [5, false, false];
    },
    // (6, _, ids) replaces all linked records with provided ids
    REPLACE_WITH: 6,
    'replace_with': function (ids) {
        return [6, false, ids];
    }
};

/**
 * Interface to be implemented by fields.
 *
 * Events:
 *     - changed_value: triggered when the value of the field has changed. This can be due
 *      to a user interaction or a call to set_value().
 *
 */
var FieldInterface = {
    /**
     * Constructor takes 2 arguments:
     * - field_manager: Implements FieldManagerMixin
     * - node: the "<field>" node in json form
     */
    init: function(field_manager, node) {},
    /**
     * Called by the form view to indicate the value of the field.
     *
     * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation,
     * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur
     * before the widget is inserted into the DOM.
     *
     * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the
     * osv class in the eCore server as well as the syntax used by the set_value() (see below). It must
     * also be able to handle any other format commonly used in the _defaults key on the models in the addons
     * as well as any format commonly returned in a on_change. It must be able to autodetect those formats as
     * no information is ever given to know which format is used.
     */
    set_value: function(value_) {},
    /**
     * Get the current value of the widget.
     *
     * Must always return a syntactically correct value to be passed to the "write" method of the osv class in
     * the eCore server, although it is not assumed to respect the constraints applied to the field.
     * For example if the field is marked as "required", a call to get_value() can return false.
     *
     * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to
     * return a default value according to the type of field.
     *
     * This method is always assumed to perform synchronously, it can not return a promise.
     *
     * If there was no user interaction to modify the value of the field, it is always assumed that
     * get_value() return the same semantic value than the one passed in the last call to set_value(),
     * although the syntax can be different. This can be the case for type of fields that have a different
     * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0).
     */
    get_value: function() {},
    /**
     * Inform the current object of the id it should use to match a html <label> that exists somewhere in the
     * view.
     */
    set_input_id: function(id) {},
    /**
     * Returns true if is_syntax_valid() returns true and the value is semantically
     * valid too according to the semantic restrictions applied to the field.
     */
    is_valid: function() {},
    /**
     * Returns true if the field holds a value which is syntactically correct, ignoring
     * the potential semantic restrictions applied to the field.
     */
    is_syntax_valid: function() {},
    /**
     * Must set the focus on the field. Return false if field is not focusable.
     */
    focus: function() {},
    /**
     * Called when the translate button is clicked.
     */
    on_translate: function() {},
    /**
        This method is called by the form view before reading on_change values and before saving. It tells
        the field to save its value before reading it using get_value(). Must return a promise.
    */
    commit_value: function() {},
    /*
        The form view call before_save before save data and if before_save return a deferred, 
        the form view wait that all deferred are resolve or fail.
        If the deferred is rejected, the field is invalidate
    */
    before_save: function() {},
};

/**
 * Abstract class for classes implementing FieldInterface.
 *
 * Properties:
 *     - value: useful property to hold the value of the field. By default, set_value() and get_value()
 *     set and retrieve the value property. Changing the value property also triggers automatically
 *     a 'changed_value' event that inform the view to trigger on_changes.
 *
 */
var AbstractField = FormWidget.extend(FieldInterface, {
    /**
     * @constructs instance.web.form.AbstractField
     * @extends instance.web.form.FormWidget
     *
     * @param field_manager
     * @param node
     */
    init: function(field_manager, node) {
        this._super(field_manager, node);
        this.name = this.node.attrs.name;
        this.field = this.field_manager.get_field_desc(this.name);
        this.widget = this.node.attrs.widget;
        this.string = this.node.attrs.string || this.field.string || this.name;
        this.options = pyeval.py_eval(this.node.attrs.options || '{}');
        this.set({'value': false});

        this.on("change:value", this, function() {
            this.trigger('changed_value');
            this._check_css_flags();
        });

        this.$translate = (_t.database.multi_lang && this.field.translate) ? $('<button/>', {
            type: 'button',
        }).addClass('o_field_translate fa fa-globe btn btn-link') : $();
    },

    renderElement: function() {
        var self = this;
        this._super();
        this.$el.addClass('o_form_field');
        this.$label = this.view ? this.view.$('label[for=' + this.id_for_label + ']') : $();
        this.do_attach_tooltip(this, this.$label[0] || this.$el);
        if (session.debug) {
            this.$label.off('dblclick').on('dblclick', function() {
                console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view);
                window.w = self;
                console.log("window.w =", window.w);
            });
        }
        if (!this.disable_utility_classes) {
            this.off("change:required", this, this._set_required);
            this.on("change:required", this, this._set_required);
            this._set_required();
        }
        this._check_visibility();
        this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags);
        this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags);
        this._check_css_flags();
    },
    start: function() {
        this._super();
        this.on("change:value", this, function() {
            if (!this.no_rerender) {
                this.render_value();
            }
            this._toggle_label();
        });
        this.on("change:effective_readonly", this, function() {
            this._toggle_label();
        });
        this.render_value();
        this._toggle_label();

        if (this.view) {
            this.$translate
                .insertAfter(this.$el)
                .on('click', _.bind(this.on_translate, this));
        }
    },
    _toggle_label: function() {
        var empty = this.get('effective_readonly') && this.is_false();
        this.$label.toggleClass('o_form_label_empty', empty).toggleClass('o_form_label_false', this.get('effective_readonly') && this.get('value') === false);
        this.$el.toggleClass('o_form_field_empty', empty);
    },
    /**
     * Private. Do not use.
     */
    _set_required: function() {
        this.$el.toggleClass('o_form_required', this.get("required"));
    },
    set_value: function(value_) {
        this.set({'value': value_});
    },
    get_value: function() {
        return this.get('value');
    },
    /**
        Utility method that all implementations should use to change the
        value without triggering a re-rendering.
    */
    internal_set_value: function(value_) {
        var tmp = this.no_rerender;
        this.no_rerender = true;
        this.set({'value': value_});
        this.no_rerender = tmp;
    },
    /**
        This method is called each time the value is modified.
    */
    render_value: function() {},
    is_valid: function() {
        return this.is_syntax_valid() && !(this.get('required') && this.is_false());
    },
    is_syntax_valid: function() {
        return true;
    },
    /**
     * Method useful to implement to ease validity testing. Must return true if the current
     * value is similar to false in eCore.
     */
    is_false: function() {
        return this.get('value') === false;
    },
    _check_css_flags: function() {
        var show_translate = (!this.get('effective_readonly') && this.field_manager.get('actual_mode') !== "create");
        this.$translate.toggleClass('o_translate_active', !!show_translate);

        this.$el.add(this.$label)
            .toggleClass('o_form_invalid', !this.disable_utility_classes && !!this.field_manager.get('display_invalid_fields') && !this.is_valid());
    },
    focus: function() {
        return false;
    },
    set_input_id: function(id) {
        this.id_for_label = id;
    },
    on_translate: function() {
        var self = this;
        var trans = new data.DataSet(this, 'ir.translation');
        return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).done(function(r) {
            self.do_action(r);
        });
    },
    set_dimensions: function(height, width) {
        this.$el.css({
            width: width,
            height: height,
        });
    },
    commit_value: function() {
        return $.when();
    },
});

/**
 * Class with everything which is common between FormViewDialog and SelectCreateDialog.
 */
var ViewDialog = Dialog.extend({ // FIXME should use ViewManager
    /**
     *  options:
     *  -readonly: only applicable when not in creation mode, default to false
     * - alternative_form_view
     * - view_id
     * - write_function
     * - read_function
     * - create_function
     * - parent_view
     * - child_name
     * - form_view_options
     */
    init: function(parent, options) {
        options = options || {};
        options.dialogClass = options.dialogClass || '';
        options.dialogClass += ' o_act_window';

        this._super(parent, $.extend(true, {}, options));

        this.res_model = options.res_model || null;
        this.res_id = options.res_id || null;
        this.domain = options.domain || [];
        this.context = options.context || {};
        this.options = _.extend(this.options || {}, options || {});

        this.on_selected = options.on_selected || (function() {});
    },

    init_dataset: function() {
        var self = this;
        this.created_elements = [];
        this.dataset = new data.ProxyDataSet(this, this.res_model, this.context);
        this.dataset.read_function = this.options.read_function;
        this.dataset.create_function = function(data, options, sup) {
            var fct = self.options.create_function || sup;
            return fct.call(this, data, options).done(function(r) {
                self.trigger('create_completed saved', r);
                self.created_elements.push(r);
            });
        };
        this.dataset.write_function = function(id, data, options, sup) {
            var fct = self.options.write_function || sup;
            return fct.call(this, id, data, options).done(function(r) {
                self.trigger('write_completed saved', r);
            });
        };
        this.dataset.parent_view = this.options.parent_view;
        this.dataset.child_name = this.options.child_name;

        this.on('closed', this, this.select);
    },

    select: function() {
        if(this.created_elements.length > 0) {
            this.on_selected(this.created_elements);
            this.created_elements = [];
        }
    }
});

/**
 * Create and edit dialog (displays a form view record and leave once saved)
 */
var FormViewDialog = ViewDialog.extend({
    init: function(parent, options) {
        var self = this;

        var multi_select = !_.isNumber(options.res_id) && !options.disable_multiple_selection;
        var readonly = _.isNumber(options.res_id) && options.readonly;

        if(!options || !options.buttons) {
            options = options || {};
            options.buttons = [
                {text: (readonly ? _t("Close") : _t("Discard")), classes: "btn-default o_form_button_cancel", close: true, click: function() {
                    self.view_form.trigger('on_button_cancel');
                }}
            ];

            if(!readonly) {
                options.buttons.splice(0, 0, {text: _t("Save") + ((multi_select)? _t(" & Close") : ""), classes: "btn-primary", click: function() {
                        self.view_form.onchanges_mutex.def.then(function() {
                            if (!self.view_form.warning_displayed) {
                                $.when(self.view_form.save()).done(function() {
                                    self.view_form.reload_mutex.exec(function() {
                                        self.trigger('record_saved');
                                        self.close();
                                    });
                                });
                            }
                        });
                    }
                });

                if(multi_select) {
                    options.buttons.splice(1, 0, {text: _t("Save & New"), classes: "btn-primary", click: function() {
                        $.when(self.view_form.save()).done(function() {
                            self.view_form.reload_mutex.exec(function() {
                                self.view_form.on_button_new();
                            });
                        });
                    }});
                }
            }
        }

        this._super(parent, options);
    },

    open: function() {
        var self = this;
        self._super();
        self.init_dataset();
        
        if (self.res_id) {
            self.dataset.ids = [self.res_id];
            self.dataset.index = 0;
        } else {
            self.dataset.index = null;
        }
        var options = _.clone(self.options.form_view_options) || {};
        if (self.res_id !== null) {
            options.initial_mode = self.options.readonly ? "view" : "edit";
        }
        _.extend(options, {
            $buttons: self.$buttons,
        });
        var FormView = core.view_registry.get('form');
        self.view_form = new FormView(self, self.dataset, self.options.view_id || false, options);
        if (self.options.alternative_form_view) {
            self.view_form.set_embedded_view(self.options.alternative_form_view);
        }

        self.do_hide();
        self.view_form.appendTo(self.$el);
        self.view_form.on("form_view_loaded", self, function() {
            self.view_form.do_show().then(function() {
                self.do_show();
                self.view_form.autofocus();
            });
        });

        return this;
    },
});

var SelectCreateListView = ListView.extend({
    do_add_record: function () {
        this.popup.create_edit_record();
    },
    select_record: function(index) {
        this.popup.on_selected([this.dataset.ids[index]]);
        this.popup.close();
    },
    do_select: function(ids, records) {
        this._super.apply(this, arguments);
        this.popup.on_click_element(ids);
    }
});

/**
 * Search dialog (displays a list of records and permits to create a new one by switching to a form view)
 */
var SelectCreateDialog = ViewDialog.extend({
    /**
     * options:
     * - initial_ids
     * - initial_view: form or search (default search)
     * - disable_multiple_selection
     * - list_view_options
     */
    init: function(parent, options) {
        this._super(parent, options);

        _.defaults(this.options, { initial_view: "search" });
        this.initial_ids = this.options.initial_ids;
    },
    
    open: function() {
        if(this.options.initial_view !== "search") {
            return this.create_edit_record();
        }

        this._super();
        this.init_dataset();
        var context = pyeval.sync_eval_domains_and_contexts({
            domains: [],
            contexts: [this.context]
        }).context;
        var search_defaults = {};
        _.each(context, function (value_, key) {
            var match = /^search_default_(.*)$/.exec(key);
            if (match) {
                search_defaults[match[1]] = value_;
            }
        });
        this.setup(search_defaults);
        return this;
    },

    setup: function(search_defaults) {
        var self = this;
        if (this.searchview) {
            this.searchview.destroy();
        }
        var $header = $('<div/>').addClass('o_modal_header').appendTo(this.$el);
        var $pager = $('<div/>').addClass('o_pager').appendTo($header);
        var $buttons = $('<div/>').addClass('o_search_options').appendTo($header);
        this.searchview = new SearchView(this, this.dataset, false,  search_defaults, {$buttons: $buttons});
        this.searchview.on('search_data', self, function(domains, contexts, groupbys) {
            if (self.initial_ids) {
                self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]),
                    contexts.concat(self.context), groupbys);
                self.initial_ids = undefined;
            } else {
                self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys);
            }
        });
        this.searchview.prependTo($header).done(function() {
            self.searchview.toggle_visibility(true);
            self.view_list = new SelectCreateListView(self,
                self.dataset, false,
                _.extend({'deletable': false,
                    'selectable': !self.options.disable_multiple_selection,
                    'import_enabled': false,
                    '$buttons': self.$buttons,
                    'disable_editable_mode': true,
                    'pager': true,
                }, self.options.list_view_options || {}));
            self.view_list.on('edit:before', self, function (e) {
                e.cancel = true;
            });
            self.view_list.popup = self;
            self.view_list.on('list_view_loaded', self, function() {
                this.on_view_list_loaded();
            });
            self.view_list.appendTo(self.$el).then(function() {
                self.view_list.do_show();
                self.view_list.render_pager($pager);
            }).then(function() {
                if (self.options.initial_facet) {
                    self.searchview.query.reset([self.options.initial_facet], {
                        preventSearch: true,
                    });
                }
                self.searchview.do_search();
            });

            var buttons = [
                {text: _t("Cancel"), classes: "btn-default o_form_button_cancel", close: true}
            ];
            if(!self.options.no_create) {
                buttons.splice(0, 0, {text: _t("Create"), classes: "btn-primary", click: function() {
                    self.create_edit_record();
                }});
            }
            if(!self.options.disable_multiple_selection) {
                buttons.splice(0, 0, {text: _t("Select"), classes: "btn-primary o_selectcreatepopup_search_select", disabled: true, close: true, click: function() {
                    self.on_selected(self.selected_ids);
                }});
            }
            self.set_buttons(buttons);
        });
    },
    do_search: function(domains, contexts, groupbys) {
        var results = pyeval.sync_eval_domains_and_contexts({
            domains: domains || [],
            contexts: contexts || [],
            group_by_seq: groupbys || []
        });
        this.view_list.do_search(results.domain, results.context, results.group_by);
    },
    on_click_element: function(ids) {
        this.selected_ids = ids || [];
        this.$footer.find(".o_selectcreatepopup_search_select").prop('disabled', this.selected_ids.length <= 0);
    },
    create_edit_record: function() {
        this.close();
        return new FormViewDialog(this.__parentedParent, this.options).open();
    },
    on_view_list_loaded: function() {},
});

var DomainEditorDialog = SelectCreateDialog.extend({
    init: function(parent, options) {
        options = _.defaults(options, {initial_facet: {
            category: _t("Custom Filter"),
            icon: 'fa-star',
            field: {
                get_context: function () { return options.context; },
                get_groupby: function () { return []; },
                get_domain: function () { return options.default_domain; },
            },
            values: [{label: _t("Selected domain"), value: null}],            
        }});

        this._super(parent, options);
    },

    get_domain: function (selected_ids) {
        var group_domain = [], domain;
        if (this.$('.o_list_record_selector input').prop('checked')) {
            if (this.view_list.grouped) {
                var group_domain = _.chain(_.values(this.view_list.groups.children))
                                        .filter(function (child) { return child.records.length; })
                                        .map(function (c) { return c.datagroup.domain;})
                                        .value();
                group_domain = _.flatten(group_domain, true);
                group_domain = _.times(group_domain.length - 1, _.constant('|')).concat(group_domain);
            }
            var search_data = this.searchview.build_search_data();
            domain = pyeval.sync_eval_domains_and_contexts({
                domains: search_data.domains,
                contexts: search_data.contexts,
                group_by_seq: search_data.groupbys || []
            }).domain;
        }
        else {
            domain = [["id", "in", selected_ids]];
        }
        return this.dataset.domain.concat(group_domain).concat(domain || []);
    },

    on_view_list_loaded: function() {
        this.$('.o_list_record_selector input').prop('checked', true);
        this.$footer.find(".o_selectcreatepopup_search_select").prop('disabled', false);
    },
});

return {
    // mixins
    FieldManagerMixin: FieldManagerMixin,
    ReinitializeWidgetMixin: ReinitializeWidgetMixin,
    ReinitializeFieldMixin: ReinitializeFieldMixin,
    CompletionFieldMixin: CompletionFieldMixin,

    // misc
    FormWidget: FormWidget,
    DefaultFieldManager: DefaultFieldManager,
    InvisibilityChanger: InvisibilityChanger,
    NotebookInvisibilityChanger: NotebookInvisibilityChanger,
    commands: commands,
    AbstractField: AbstractField,

    // Dialogs
    ViewDialog: ViewDialog,
    FormViewDialog: FormViewDialog,
    SelectCreateDialog: SelectCreateDialog,
    DomainEditorDialog: DomainEditorDialog,
};

});
Beispiel #5
0
odoo.define('web.CrashManager', function (require) {
"use strict";

var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');
var session = require('web.session');

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

var map_title ={
    user_error: _lt('Warning'),
    warning: _lt('Warning'),
    access_error: _lt('Access Error'),
    missing_error: _lt('Missing Record'),
    validation_error: _lt('Validation Error'),
    except_orm: _lt('Global Business Error'),
    access_denied: _lt('Access Denied'),
};

var CrashManager = core.Class.extend({
    init: function() {
        this.active = true;
    },
    enable: function () {
        this.active = true;
    },
    disable: function () {
        this.active = false;
    },
    rpc_error: function(error) {
        var self = this;
        if (!this.active) {
            return;
        }
        if (this.$indicator){
            return;
        }
        if (error.code == -32098) {
            $.blockUI({ message: '' , overlayCSS: {'z-index': 9999, backgroundColor: '#FFFFFF', opacity: 0.0, cursor: 'wait'}});
            this.$indicator = $('<div class="oe_indicator">' + _t("Trying to reconnect... ") + '<i class="fa fa-refresh"></i></div>');
            this.$indicator.prependTo("body");
            var timeinterval = setInterval(function(){
                ajax.jsonRpc('/web/webclient/version_info').then(function() {
                    clearInterval(timeinterval);
                    self.$indicator.html(_t("You are back online"));
                    self.$indicator.delay(2000).fadeOut('slow',function(){
                        $(this).remove();
                        self.$indicator.remove();
                    });
                    $.unblockUI();
                });
            }, 2000);
            return;
        }
        var handler = core.crash_registry.get(error.data.name, true);
        if (handler) {
            new (handler)(this, error).display();
            return;
        }
        if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
            this.show_warning({type: "Session Expired", data: { message: _t("Your Odoo session expired. Please refresh the current web page.") }});
            return;
        }
        if (_.has(map_title, error.data.exception_type)) {
            if(error.data.exception_type == 'except_orm'){
                if(error.data.arguments[1]) {
                    error = _.extend({}, error,
                                {
                                    data: _.extend({}, error.data,
                                        {
                                            message: error.data.arguments[1],
                                            title: error.data.arguments[0] !== 'Warning' ? (" - " + error.data.arguments[0]) : '',
                                        })
                                });
                }
                else {
                    error = _.extend({}, error,
                                {
                                    data: _.extend({}, error.data,
                                        {
                                            message: error.data.arguments[0],
                                            title:  '',
                                        })
                                });
                }
            }
            else {
                error = _.extend({}, error,
                            {
                                data: _.extend({}, error.data,
                                    {
                                        message: error.data.arguments[0],
                                        title: map_title[error.data.exception_type] !== 'Warning' ? (" - " + map_title[error.data.exception_type]) : '',
                                    })
                            });
            }

            this.show_warning(error);
        //InternalError    

        } else {
            this.show_error(error);
        }
    },
    show_warning: function(error) {
        if (!this.active) {
            return;
        }
        new Dialog(this, {
            size: 'medium',
            title: "Odoo " + (_.str.capitalize(error.type) || _t("Warning")),
            subtitle: error.data.title,
            $content: $('<div>').html(QWeb.render('CrashManager.warning', {error: error}))
        }).open();
    },
    show_error: function(error) {
        if (!this.active) {
            return;
        }
        new Dialog(this, {
            title: "Odoo " + _.str.capitalize(error.type),
            $content: QWeb.render('CrashManager.error', {session: session, error: error})
        }).open();
    },
    show_message: function(exception) {
        this.show_error({
            type: _t("Client Error"),
            message: exception,
            data: {debug: ""}
        });
    },
});

/**
    An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
*/
var ExceptionHandler = {
    /**
        @param parent The parent.
        @param error The error object as returned by the JSON-RPC implementation.
    */
    init: function(parent, error) {},
    /**
        Called to inform to display the widget, if necessary. A typical way would be to implement
        this interface in a class extending instance.web.Dialog and simply display the dialog in this
        method.
    */
    display: function() {},
};


/**
 * Handle redirection warnings, which behave more or less like a regular
 * warning, with an additional redirection button.
 */
var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
    init: function(parent, error) {
        this._super(parent);
        this.error = error;
    },
    display: function() {
        var self = this;
        var error = this.error;
        error.data.message = error.data.arguments[0];

        new Dialog(this, {
            size: 'medium',
            title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
            buttons: [
                {text: error.data.arguments[2], classes : "btn-primary", click: function() {
                    window.location.href = '#action='+error.data.arguments[1];
                    self.destroy();
                }},
                {text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
            ],
            $content: QWeb.render('CrashManager.warning', {error: error})
        }).open();
    }
});

core.crash_registry.add('openerp.exceptions.RedirectWarning', RedirectWarningHandler);

return CrashManager;
});
Beispiel #6
0
odoo.define('web.Widget', function (require) {
"use strict";

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

var qweb = core.qweb;
var mixins = core.mixins;

/**
 * Base class for all visual components. Provides a lot of functionalities helpful
 * for the management of a part of the DOM.
 *
 * Widget handles:
 * - Rendering with QWeb.
 * - Life-cycle management and parenting (when a parent is destroyed, all its children are
 *     destroyed too).
 * - Insertion in DOM.
 *
 * Guide to create implementations of the Widget class:
 * ==============================================
 *
 * Here is a sample child class:
 *
 * MyWidget = openerp.base.Widget.extend({
 *     // the name of the QWeb template to use for rendering
 *     template: "MyQWebTemplate",
 *
 *     init: function(parent) {
 *         this._super(parent);
 *         // stuff that you want to init before the rendering
 *     },
 *     start: function() {
 *         // stuff you want to make after the rendering, `this.$el` holds a correct value
 *         this.$el.find(".my_button").click(/* an example of event binding * /);
 *
 *         // if you have some asynchronous operations, it's a good idea to return
 *         // a promise in start()
 *         var promise = this.rpc(...);
 *         return promise;
 *     }
 * });
 *
 * Now this class can simply be used with the following syntax:
 *
 * var my_widget = new MyWidget(this);
 * my_widget.appendTo($(".some-div"));
 *
 * With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
 * DOM inside the ".some-div" div and its events were binded.
 *
 * And of course, when you don't need that widget anymore, just do:
 *
 * my_widget.destroy();
 *
 * That will kill the widget in a clean way and erase its content from the dom.
 */


var Widget = core.Class.extend(mixins.PropertiesMixin, {
    // Backbone-ish API
    tagName: 'div',
    id: null,
    className: null,
    attributes: {},
    events: {},
    custom_events: {},
    /**
     * The name of the QWeb template that will be used for rendering. Must be
     * redefined in subclasses or the default render() method can not be used.
     *
     * @type string
     */
    template: null,
    /**
     * Constructs the widget and sets its parent if a parent is given.
     *
     * @constructs openerp.Widget
     *
     * @param {openerp.Widget} parent Binds the current instance to the given Widget instance.
     * When that widget is destroyed by calling destroy(), the current instance will be
     * destroyed too. Can be null.
     */
    init: function(parent) {
        mixins.PropertiesMixin.init.call(this);
        this.setParent(parent);
        // Bind on_/do_* methods to this
        // We might remove this automatic binding in the future
        for (var name in this) {
            if(typeof(this[name]) == "function") {
                if((/^on_|^do_/).test(name)) {
                    this[name] = _.bind(this[name], this);
                }
            }
        }
        // FIXME: this should not be
        this.setElement(this._make_descriptive());
        this.delegateCustomEvents();
    },
    /**
     * Method called between init and start. Performs asynchronous calls required by start.
     *
     * This method should return a Deferred which is resolved when start can be executed.
     *
     * @return {jQuery.Deferred}
     */
    willStart: function() {
        return $.when();
    },
    /**
     * Destroys the current widget, also destroys all its children before destroying itself.
     */
    destroy: function() {
        _.each(this.getChildren(), function(el) {
            el.destroy();
        });
        if(this.$el) {
            this.$el.remove();
        }
        mixins.PropertiesMixin.destroy.call(this);
    },
    /**
     * Renders the current widget and appends it to the given jQuery object or Widget.
     *
     * @param target A jQuery object or a Widget instance.
     */
    appendTo: function(target) {
        var self = this;
        return this.__widgetRenderAndInsert(function(t) {
            self.$el.appendTo(t);
        }, target);
    },
    /**
     * Renders the current widget and prepends it to the given jQuery object or Widget.
     *
     * @param target A jQuery object or a Widget instance.
     */
    prependTo: function(target) {
        var self = this;
        return this.__widgetRenderAndInsert(function(t) {
            self.$el.prependTo(t);
        }, target);
    },
    /**
     * Renders the current widget and inserts it after to the given jQuery object or Widget.
     *
     * @param target A jQuery object or a Widget instance.
     */
    insertAfter: function(target) {
        var self = this;
        return this.__widgetRenderAndInsert(function(t) {
            self.$el.insertAfter(t);
        }, target);
    },
    /**
     * Renders the current widget and inserts it before to the given jQuery object or Widget.
     *
     * @param target A jQuery object or a Widget instance.
     */
    insertBefore: function(target) {
        var self = this;
        return this.__widgetRenderAndInsert(function(t) {
            self.$el.insertBefore(t);
        }, target);
    },
    /**
     * Attach the current widget to a dom element
     *
     * @param target A jQuery object or a Widget instance.
     */
    attachTo: function(target) {
        var self = this;
        this.setElement(target.$el || target);
        return this.willStart().then(function() {
            return self.start();
        });
    },
    /**
     * Renders the current widget and replaces the given jQuery object.
     *
     * @param target A jQuery object or a Widget instance.
     */
    replace: function(target) {
        return this.__widgetRenderAndInsert(_.bind(function(t) {
            this.$el.replaceAll(t);
        }, this), target);
    },
    __widgetRenderAndInsert: function(insertion, target) {
        var self = this;
        return this.willStart().then(function() {
            self.renderElement();
            insertion(target);
            return self.start();
        });
    },
    /**
     * Method called after rendering. Mostly used to bind actions, perform asynchronous
     * calls, etc...
     *
     * By convention, this method should return an object that can be passed to $.when() 
     * to inform the caller when this widget has been initialized.
     *
     * @returns {jQuery.Deferred or any}
     */
    start: function() {
        return $.when();
    },
    /**
     * Renders the element. The default implementation renders the widget using QWeb,
     * `this.template` must be defined. The context given to QWeb contains the "widget"
     * key that references `this`.
     */
    renderElement: function() {
        var $el;
        if (this.template) {
            $el = $(qweb.render(this.template, {widget: this}).trim());
        } else {
            $el = this._make_descriptive();
        }
        this.replaceElement($el);
    },
    /**
     * Re-sets the widget's root element and replaces the old root element
     * (if any) by the new one in the DOM.
     *
     * @param {HTMLElement | jQuery} $el
     * @returns {*} this
     */
    replaceElement: function ($el) {
        var $oldel = this.$el;
        this.setElement($el);
        if ($oldel && !$oldel.is(this.$el)) {
            if ($oldel.length > 1) {
                $oldel.wrapAll('<div/>');
                $oldel.parent().replaceWith(this.$el);
            } else {
                $oldel.replaceWith(this.$el);
            }
        }
        return this;
    },
    /**
     * Re-sets the widget's root element (el/$el/$el).
     *
     * Includes:
     * * re-delegating events
     * * re-binding sub-elements
     * * if the widget already had a root element, replacing the pre-existing
     *   element in the DOM
     *
     * @param {HTMLElement | jQuery} element new root element for the widget
     * @return {*} this
     */
    setElement: function (element) {
        // NB: completely useless, as WidgetMixin#init creates a $el
        // always
        if (this.$el) {
            this.undelegateEvents();
        }

        this.$el = (element instanceof $) ? element : $(element);
        this.el = this.$el[0];

        this.delegateEvents();

        return this;
    },
    /**
     * Utility function to build small DOM elements.
     *
     * @param {String} tagName name of the DOM element to create
     * @param {Object} [attributes] map of DOM attributes to set on the element
     * @param {String} [content] HTML content to set on the element
     * @return {Element}
     */
    make: function (tagName, attributes, content) {
        var el = document.createElement(tagName);
        if (!_.isEmpty(attributes)) {
            $(el).attr(attributes);
        }
        if (content) {
            $(el).html(content);
        }
        return el;
    },
    /**
     * Makes a potential root element from the declarative builder of the
     * widget
     *
     * @return {jQuery}
     * @private
     */
    _make_descriptive: function () {
        var attrs = _.extend({}, this.attributes || {});
        if (this.id) { attrs.id = this.id; }
        if (this.className) { attrs['class'] = this.className; }
        return $(this.make(this.tagName, attrs));
    },
    delegateEvents: function () {
        var events = this.events;
        if (_.isEmpty(events)) { return; }

        for(var key in events) {
            if (!events.hasOwnProperty(key)) { continue; }

            var method = this.proxy(events[key]);

            var match = /^(\S+)(\s+(.*))?$/.exec(key);
            var event = match[1];
            var selector = match[3];

            event += '.widget_events';
            if (!selector) {
                this.$el.on(event, method);
            } else {
                this.$el.on(event, selector, method);
            }
        }
    },
    delegateCustomEvents: function () {
        if (_.isEmpty(this.custom_events)) { return; }
        for (var key in this.custom_events) {
            if (!this.custom_events.hasOwnProperty(key)) { continue; }

            var method = this.proxy(this.custom_events[key]);
            this.on(key, this, wrap_handler(method));
        }
        function wrap_handler(callback) {
            return function (event) {
                event.stop_propagation();
                callback(event);
            };
        }
    },
    undelegateEvents: function () {
        this.$el.off('.widget_events');
    },
    /**
     * Shortcut for ``this.$el.find(selector)``
     *
     * @param {String} selector CSS selector, rooted in $el
     * @returns {jQuery} selector match
     */
    $: function(selector) {
        if (selector === undefined)
            return this.$el;
        return this.$el.find(selector);
    },
    /**
     * Displays the widget
     */
    do_show: function () {
        this.$el.removeClass('o_hidden');
    },
    /**
     * Hides the widget
     */
    do_hide: function () {
        this.$el.addClass('o_hidden');
    },
    /**
     * Displays or hides the widget
     * @param {Boolean} [display] use true to show the widget or false to hide it
     */
    do_toggle: function (display) {
        if (_.isBoolean(display)) {
            display ? this.do_show() : this.do_hide();
        } else {
            this.$el.hasClass('o_hidden') ? this.do_show() : this.do_hide();
        }
    },
    /**
     * Proxies a method of the object, in order to keep the right ``this`` on
     * method invocations.
     *
     * This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
     * even more so to ``jQuery.proxy`` with a fundamental difference: its
     * resolution of the method being called is lazy, meaning it will use the
     * method as it is when the proxy is called, not when the proxy is created.
     *
     * Other methods will fix the bound method to what it is when creating the
     * binding/proxy, which is fine in most javascript code but problematic in
     * OpenERP Web where developers may want to replace existing callbacks with
     * theirs.
     *
     * The semantics of this precisely replace closing over the method call.
     *
     * @param {String|Function} method function or name of the method to invoke
     * @returns {Function} proxied method
     */
    proxy: function (method) {
        var self = this;
        return function () {
            var fn = (typeof method === 'string') ? self[method] : method;
            return fn.apply(self, arguments);
        };
    },
    /**
     * Informs the action manager to do an action. This supposes that
     * the action manager can be found amongst the ancestors of the current widget.
     * If that's not the case this method will simply return `false`.
     */
    do_action: function() {
        var parent = this.getParent();
        if (parent) {
            return parent.do_action.apply(parent, arguments);
        }
        return false;
    },
    do_notify: function(title, message, sticky) {
        this.trigger_up('notification', {title: title, message: message, sticky: sticky});
    },
    do_warn: function(title, message, sticky) {
        this.trigger_up('warning', {title: title, message: message, sticky: sticky});
    },
    rpc: function(url, data, options) {
        return this.alive(session.rpc(url, data, options));
    },
});

return Widget;

});
Beispiel #7
0
odoo.define('pos_restaurant.multiprint', function (require) {
"use strict";

var models = require('point_of_sale.models');
var screens = require('point_of_sale.screens');
var core = require('web.core');
var mixins = require('web.mixins');
var Session = require('web.Session');

var QWeb = core.qweb;

var Printer = core.Class.extend(mixins.PropertiesMixin,{
    init: function(parent,options){
        mixins.PropertiesMixin.init.call(this);
        this.setParent(parent);
        options = options || {};
        var url = options.url || 'http://localhost:8069';
        this.connection = new Session(undefined,url, { use_cors: true});
        this.host       = url;
        this.receipt_queue = [];
    },
    print: function(receipt){
        var self = this;
        if(receipt){
            this.receipt_queue.push(receipt);
        }
        function send_printing_job(){
            if(self.receipt_queue.length > 0){
                var r = self.receipt_queue.shift();
                self.connection.rpc('/hw_proxy/print_xml_receipt',{receipt: r},{timeout: 5000})
                    .then(function(){
                        send_printing_job();
                    },function(){
                        self.receipt_queue.unshift(r);
                    });
            }
        }
        send_printing_job();
    },
});

models.load_models({
    model: 'restaurant.printer',
    fields: ['name','proxy_ip','product_categories_ids'],
    domain: null,
    loaded: function(self,printers){
        var active_printers = {};
        for (var i = 0; i < self.config.printer_ids.length; i++) {
            active_printers[self.config.printer_ids[i]] = true;
        }

        self.printers = [];
        self.printers_categories = {}; // list of product categories that belong to
                                       // one or more order printer

        for(var i = 0; i < printers.length; i++){
            if(active_printers[printers[i].id]){
                var url = printers[i].proxy_ip;
                if(url.indexOf('//') < 0){
                    url = 'http://'+url;
                }
                if(url.indexOf(':',url.indexOf('//')+2) < 0){
                    url = url+':8069';
                }
                var printer = new Printer(self,{url:url});
                printer.config = printers[i];
                self.printers.push(printer);

                for (var j = 0; j < printer.config.product_categories_ids.length; j++) {
                    self.printers_categories[printer.config.product_categories_ids[j]] = true;
                }
            }
        }
        self.printers_categories = _.keys(self.printers_categories);
        self.config.iface_printers = !!self.printers.length;
    },
});

var _super_orderline = models.Orderline.prototype;

models.Orderline = models.Orderline.extend({
    initialize: function() {
        _super_orderline.initialize.apply(this,arguments);
        if (!this.pos.config.iface_printers) {
            return;
        }
        if (typeof this.mp_dirty === 'undefined') {
            // mp dirty is true if this orderline has changed
            // since the last kitchen print
            // it's left undefined if the orderline does not
            // need to be printed to a printer. 

            this.mp_dirty = this.printable() || undefined;
        } 
        if (!this.mp_skip) {
            // mp_skip is true if the cashier want this orderline
            // not to be sent to the kitchen
            this.mp_skip  = false;
        }
    },
    // can this orderline be potentially printed ? 
    printable: function() {
        return this.pos.db.is_product_in_category(this.pos.printers_categories, this.get_product().id);
    },
    init_from_JSON: function(json) {
        _super_orderline.init_from_JSON.apply(this,arguments);
        this.mp_dirty = json.mp_dirty;
        this.mp_skip  = json.mp_skip;
    },
    export_as_JSON: function() {
        var json = _super_orderline.export_as_JSON.apply(this,arguments);
        json.mp_dirty = this.mp_dirty;
        json.mp_skip  = this.mp_skip;
        return json;
    },
    set_quantity: function(quantity) {
        if (this.pos.config.iface_printers && quantity !== this.quantity && this.printable()) {
            this.mp_dirty = true;
        }
        _super_orderline.set_quantity.apply(this,arguments);
    },
    can_be_merged_with: function(orderline) { 
        return (!this.mp_skip) && 
               (!orderline.mp_skip) &&
               _super_orderline.can_be_merged_with.apply(this,arguments);
    },
    set_skip: function(skip) {
        if (this.mp_dirty && skip && !this.mp_skip) {
            this.mp_skip = true;
            this.trigger('change',this);
        }
        if (this.mp_skip && !skip) {
            this.mp_dirty = true;
            this.mp_skip  = false;
            this.trigger('change',this);
        }
    },
    set_dirty: function(dirty) {
        this.mp_dirty = dirty;
        this.trigger('change',this);
    },
    get_line_diff_hash: function(){
        if (this.get_note()) {
            return this.id + '|' + this.get_note();
        } else {
            return '' + this.id;
        }
    },
});

screens.OrderWidget.include({
    render_orderline: function(orderline) {
        var node = this._super(orderline);
        if (this.pos.config.iface_printers) {
            if (orderline.mp_skip) {
                node.classList.add('skip');
            } else if (orderline.mp_dirty) {
                node.classList.add('dirty');
            }
        }
        return node;
    },
    click_line: function(line, event) {
        if (!this.pos.config.iface_printers) {
            this._super(line, event);
        } else if (this.pos.get_order().selected_orderline !== line) {
            this.mp_dbclk_time = (new Date()).getTime();
        } else if (!this.mp_dbclk_time) {
            this.mp_dbclk_time = (new Date()).getTime();
        } else if (this.mp_dbclk_time + 500 > (new Date()).getTime()) {
            line.set_skip(!line.mp_skip);
            this.mp_dbclk_time = 0;
        } else {
            this.mp_dbclk_time = (new Date()).getTime();
        }

        this._super(line, event);
    },
});

var _super_order = models.Order.prototype;
models.Order = models.Order.extend({
    build_line_resume: function(){
        var resume = {};
        this.orderlines.each(function(line){
            if (line.mp_skip) {
                return;
            }
            var line_hash = line.get_line_diff_hash();
            var qty  = Number(line.get_quantity());
            var note = line.get_note();
            var product_id = line.get_product().id;

            if (typeof resume[line_hash] === 'undefined') {
                resume[line_hash] = {
                    qty: qty,
                    note: note,
                    product_id: product_id,
                    product_name_wrapped: line.generate_wrapped_product_name(),
                };
            } else {
                resume[line_hash].qty += qty;
            }

        });
        return resume;
    },
    saveChanges: function(){
        this.saved_resume = this.build_line_resume();
        this.orderlines.each(function(line){
            line.set_dirty(false);
        });
        this.trigger('change',this);
    },
    computeChanges: function(categories){
        var current_res = this.build_line_resume();
        var old_res     = this.saved_resume || {};
        var json        = this.export_as_JSON();
        var add = [];
        var rem = [];
        var line_hash;

        for ( line_hash in current_res) {
            var curr = current_res[line_hash];
            var old  = old_res[line_hash];

            if (typeof old === 'undefined') {
                add.push({
                    'id':       curr.product_id,
                    'name':     this.pos.db.get_product_by_id(curr.product_id).display_name,
                    'name_wrapped': curr.product_name_wrapped,
                    'note':     curr.note,
                    'qty':      curr.qty,
                });
            } else if (old.qty < curr.qty) {
                add.push({
                    'id':       curr.product_id,
                    'name':     this.pos.db.get_product_by_id(curr.product_id).display_name,
                    'name_wrapped': curr.product_name_wrapped,
                    'note':     curr.note,
                    'qty':      curr.qty - old.qty,
                });
            } else if (old.qty > curr.qty) {
                rem.push({
                    'id':       curr.product_id,
                    'name':     this.pos.db.get_product_by_id(curr.product_id).display_name,
                    'name_wrapped': curr.product_name_wrapped,
                    'note':     curr.note,
                    'qty':      old.qty - curr.qty,
                });
            }
        }

        for (line_hash in old_res) {
            if (typeof current_res[line_hash] === 'undefined') {
                var old = old_res[line_hash];
                rem.push({
                    'id':       old.product_id,
                    'name':     this.pos.db.get_product_by_id(old.product_id).display_name,
                    'name_wrapped': old.product_name_wrapped,
                    'note':     old.note,
                    'qty':      old.qty, 
                });
            }
        }

        if(categories && categories.length > 0){
            // filter the added and removed orders to only contains
            // products that belong to one of the categories supplied as a parameter

            var self = this;

            var _add = [];
            var _rem = [];
            
            for(var i = 0; i < add.length; i++){
                if(self.pos.db.is_product_in_category(categories,add[i].id)){
                    _add.push(add[i]);
                }
            }
            add = _add;

            for(var i = 0; i < rem.length; i++){
                if(self.pos.db.is_product_in_category(categories,rem[i].id)){
                    _rem.push(rem[i]);
                }
            }
            rem = _rem;
        }

        var d = new Date();
        var hours   = '' + d.getHours();
            hours   = hours.length < 2 ? ('0' + hours) : hours;
        var minutes = '' + d.getMinutes();
            minutes = minutes.length < 2 ? ('0' + minutes) : minutes;

        return {
            'new': add,
            'cancelled': rem,
            'table': json.table || false,
            'floor': json.floor || false,
            'name': json.name  || 'unknown order',
            'time': {
                'hours':   hours,
                'minutes': minutes,
            },
        };
        
    },
    printChanges: function(){
        var printers = this.pos.printers;
        for(var i = 0; i < printers.length; i++){
            var changes = this.computeChanges(printers[i].config.product_categories_ids);
            if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
                var receipt = QWeb.render('OrderChangeReceipt',{changes:changes, widget:this});
                printers[i].print(receipt);
            }
        }
    },
    hasChangesToPrint: function(){
        var printers = this.pos.printers;
        for(var i = 0; i < printers.length; i++){
            var changes = this.computeChanges(printers[i].config.product_categories_ids);
            if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
                return true;
            }
        }
        return false;
    },
    hasSkippedChanges: function() {
        var orderlines = this.get_orderlines();
        for (var i = 0; i < orderlines.length; i++) {
            if (orderlines[i].mp_skip) {
                return true;
            }
        }
        return false;
    },
    export_as_JSON: function(){
        var json = _super_order.export_as_JSON.apply(this,arguments);
        json.multiprint_resume = this.saved_resume;
        return json;
    },
    init_from_JSON: function(json){
        _super_order.init_from_JSON.apply(this,arguments);
        this.saved_resume = json.multiprint_resume;
    },
});

var SubmitOrderButton = screens.ActionButtonWidget.extend({
    'template': 'SubmitOrderButton',
    button_click: function(){
        var order = this.pos.get_order();
        if(order.hasChangesToPrint()){
            order.printChanges();
            order.saveChanges();
        }
    },
});

screens.define_action_button({
    'name': 'submit_order',
    'widget': SubmitOrderButton,
    'condition': function() {
        return this.pos.printers.length;
    },
});

screens.OrderWidget.include({
    update_summary: function(){
        this._super();
        var changes = this.pos.get_order().hasChangesToPrint();
        var skipped = changes ? false : this.pos.get_order().hasSkippedChanges();
        var buttons = this.getParent().action_buttons;

        if (buttons && buttons.submit_order) {
            buttons.submit_order.highlight(changes);
            buttons.submit_order.altlight(skipped);
        }
    },
});

return {
    Printer: Printer,
    SubmitOrderButton: SubmitOrderButton,
}

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

var ActionManager = require('web.ActionManager');
var CrashManager = require('web.CrashManager');
var core = require('web.core');
var FormView = require('web.FormView');
var Session = require('web.Session');
var session = require('web.session');
var View = require('web.View');
var web_client = require('web.web_client');
var WebClient = require('web.WebClient');

/*
*  The Web Analytics Module inserts the Google Analytics JS Snippet
*  at the top of the page, and sends to google an url each time the
*  openerp url is changed.
*  The pushes of the urls is made by triggering the 'state_pushed' event in the
*  web_client.do_push_state() method which is responsible of changing the openerp current url
*/

// Google Analytics Code snippet
(function() {
    var ga   = document.createElement('script');
    ga.type  = 'text/javascript';
    ga.async = true;
    ga.src   = ('https:' === document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(ga,s);
})();

var Tracker = core.Class.extend({
    /*
    * This method initializes the tracker
    */
    init: function(webclient) {
        var self = this;
        self.initialized = $.Deferred();
        _gaq.push(['_setAccount', 'UA-7333765-1']);
        _gaq.push(['_setDomainName', '.openerp.com']);  // Allow multi-domain
        self.initialize_custom(webclient).then(function() {
            webclient.on('state_pushed', self, self.on_state_pushed);
            self.include_tracker();
        });
    },
    /*
    * This method MUST be overriden by saas_demo and saas_trial in order to
    * set the correct user type. By default, the user connected is local to
    * the DB (like in accounts).
    */
    _get_user_type: function() {
        return 'Local User';
    },
    /*
    * This method gets the user access level, to be used as CV in GA
    */
    _get_user_access_level: function() {
        if (!session.session_is_valid()) {
            return "Unauthenticated User";
        }
        if (session.uid === 1) {
            return 'Admin User';
        }
        // Make the difference between portal users and anonymous users
        if (session.username.indexOf('@') !== -1) {
            return 'Portal User';
        }
        if (session.username === 'anonymous') {
            return 'Anonymous User';
        }
        return 'Normal User';
    },
    
    /*
    * This method contains the initialization of all user-related custom variables
    * stored in GA. Also other modules can override it to add new custom variables
    * Must be followed by a call to _push_*() in order to actually send the data
    * to GA.
    */
    initialize_custom: function() {
        var self = this;
        session.rpc("/web/webclient/version_info", {})
            .done(function(res) {
                _gaq.push(['_setCustomVar', 5, 'Version', res.server_version, 3]);
                self._push_customvars();
                self.initialized.resolve(self);
            });
        return self.initialized;
    },

    /*
     * Method called in order to send _setCustomVar to GA
     */
    _push_customvars: function() {
        var self = this;
        // Track User Access Level, Custom Variable 4 in GA with visitor level scope
        // Values: 'Admin User', 'Normal User', 'Portal User', 'Anonymous User'
        _gaq.push(['_setCustomVar', 4, 'User Access Level', self._get_user_access_level(), 1]);

        // Track User Type Conversion, Custom Variable 3 in GA with session level scope
        // Values: 'Visitor', 'Demo', 'Online Trial', 'Online Paying', 'Local User'
        _gaq.push(['_setCustomVar', 1, 'User Type Conversion', self._get_user_type(), 2]);
    },
    
    /*
    * Method called in order to send _trackPageview to GA
    */
    _push_pageview: function(url) {
        _gaq.push(['_trackPageview', url]);
    },
    /*
    * Method called in order to send _trackEvent to GA
    */
    _push_event: function(options) {
        _gaq.push(['_trackEvent',
            options.category,
            options.action,
            options.label,
            options.value,
            options.noninteraction
        ]);
    },
    /*
    * Method called in order to send ecommerce transactions to GA
    */
    _push_ecommerce: function(trans_data, item_list) {
        _gaq.push(['_addTrans',
            trans_data.order_id,
            trans_data.store_name,
            trans_data.total,
            trans_data.tax,
            trans_data.shipping,
            trans_data.city,
            trans_data.state,
            trans_data.country,
        ]);
        _.each(item_list, function(item) {
            _gaq.push(['_addItem',
                item.order_id,
                item.sku,
                item.name,
                item.category,
                item.price,
                item.quantity,
            ]);
        });
        _gaq.push(['_trackTrans']);
    },
    /*
    *  This method contains the initialization of the object and view type
    *  as an event in GA.
    */
    on_state_pushed: function(state) {
        // Track only pages corresponding to a 'normal' view of OpenERP, views
        // related to client actions are tracked by the action manager
        if (state.model && state.view_type) {
            // Track the page
            var url = generateUrl({'model': state.model, 'view_type': state.view_type});
            this._push_event({
                'category': state.model,
                'action': state.view_type,
                'label': url,
            });
            this._push_pageview(url);
        }
    },
    /*
    * This method includes the tracker into views and managers. It can be overriden
    * by other modules in order to extend tracking functionalities
    */
    include_tracker: function() {
        var t = this;
        // Track the events related with the creation and the  modification of records,
        // the view type is always form
        FormView.include({
            init: function(parent, dataset, view_id, options) {
                this._super.apply(this, arguments);
                var self = this;
                this.on('record_created', self, function(r) {
                    var url = generateUrl({'model': self.model, 'view_type': 'form'});
                    t._push_event({
                        'category': self.model,
                        'action': 'create',
                        'label': url,
                        'noninteraction': true,
                    });
                });
                this.on('record_saved', self, function(r) {
                    var url = generateUrl({'model': self.model, 'view_type': 'form'});
                    t._push_event({
                        'category': self.model,
                        'action': 'save',
                        'label': url,
                        'noninteraction': true,
                    });
                });
            }
        });

        // Track client actions
        ActionManager.include({
            ir_actions_client: function (action, options) {
                var url = generateUrl({'action': action.tag});
                t._push_event({
                    'category': action.type,
                    'action': action.tag,
                    'label': url,
                });
                t._push_pageview(url);
                return this._super.apply(this, arguments);
            },
        });

        // Track button events
        View.include({
            do_execute_action: function(action_data, dataset, record_id, on_closed) {
                var category = this.model || dataset.model || '';
                var action;
                if (action_data.name && _.isNaN(action_data.name-0)) {
                    action = action_data.name;
                } else {
                    action = action_data.string || action_data.special || '';
                }
                var url = generateUrl({'model': category, 'view_type': this.view_type});
                t._push_event({
                    'category': category,
                    'action': action,
                    'label': url,
                    'noninteraction': true,
                });
                return this._super.apply(this, arguments);
            },
        });

        // Track error events
        CrashManager.include({
            show_error: function(error) {
                var hash = window.location.hash;
                var params = $.deparam(hash.substr(hash.indexOf('#')+1));
                var options = {};
                if (params.model && params.view_type) {
                    options = {'model': params.model, 'view_type': params.view_type};
                } else {
                    options = {'action': params.action};
                }
                var url = generateUrl(options);
                t._push_event({
                    'category': options.model || "ir.actions.client",
                    'action': "error " + (error.code ? error.message + error.data.message : error.type + error.data.debug),
                    'label': url,
                    'noninteraction': true,
                });
                this._super.apply(this, arguments);
            },
        });
    },
});

// ----------------------------------------------------------------
// utility functions
// ----------------------------------------------------------------

var generateUrl = function(options) {
    var url = '';
    var keys = _.keys(options);
    keys = _.sortBy(keys, function(i) { return i;});
    _.each(keys, function(key) {
        url += '/' + key + '/' + options[key];
    });
    return url;
};

// kept for API compatibility
var setupTracker = function(wc) {
    return wc.tracker.initialized;
};

WebClient.include({
    bind_events: function() {
        this._super.apply(this, arguments);
        this.tracker = new Tracker(this);
    },
});

Session.include({
    session_authenticate: function() {
        return $.when(this._super.apply(this, arguments)).then(function() {
            web_client.tracker._push_customvars();
        });
    },
});
 
});
odoo.define('pos_keyboard.pos', function (require) {
    "use strict";

    var core = require('web.core');
    var models = require('point_of_sale.models');
    var screens = require('point_of_sale.screens');

    var _super_posmodel = models.PosModel.prototype;
    models.PosModel = models.PosModel.extend({
        initialize: function (session, attributes) {
            this.keypad = new Keypad({'pos': this});
            return _super_posmodel.initialize.call(this, session, attributes);
        }
    });

    screens.NumpadWidget.include({
        start: function() {
            this._super();
            var self = this;
            this.pos.keypad.set_action_callback(function(data){
                 self.keypad_action(data, self.pos.keypad.type);
            });
        },
        keypad_action: function(data, type){
             if (data.type === type.numchar){
                 this.state.appendNewChar(data.val);
             }
             else if (data.type === type.bmode) {
                 this.state.changeMode(data.val);
             }
             else if (data.type === type.sign){
                 this.clickSwitchSign();
             }
             else if (data.type === type.backspace){
                 this.clickDeleteLastChar();
             }
        }
    });
    
    screens.PaymentScreenWidget.include({
        show: function(){
            this._super();
            this.pos.keypad.disconnect();
        },
        hide: function(){
            this._super();
            this.pos.keypad.connect();
        }
    });
    
    // this module mimics a keypad-only cash register. Use connect() and 
    // disconnect() to activate and deactivate it.
    var Keypad = core.Class.extend({
        init: function(attributes){
            this.pos = attributes.pos;
            /*this.pos_widget = this.pos.pos_widget;*/
            this.type = {
                numchar: 'number, dot',
                bmode: 'quantity, discount, price',
                sign: '+, -',
                backspace: 'backspace'
            };
            this.data = {
                type: undefined,
                val: undefined
            };
            this.action_callback = undefined;
        },

        save_callback: function(){
            this.saved_callback_stack.push(this.action_callback);
        },

        restore_callback: function(){
            if (this.saved_callback_stack.length > 0) {
                this.action_callback = this.saved_callback_stack.pop();
            }
        },

        set_action_callback: function(callback){
            this.action_callback = callback
        },

        //remove action callback
        reset_action_callback: function(){
            this.action_callback = undefined;
        },

        // starts catching keyboard events and tries to interpret keystrokes,
        // calling the callback when needed.
        connect: function(){
            var self = this;
            // --- additional keyboard ---//
            var KC_PLU = 107;      // KeyCode: + or - (Keypad '+')
            var KC_QTY = 111;      // KeyCode: Quantity (Keypad '/')
            var KC_AMT = 106;      // KeyCode: Price (Keypad '*')
            var KC_DISC = 109;     // KeyCode: Discount Percentage [0..100] (Keypad '-')
            // --- basic keyboard --- //
            var KC_PLU_1 = 83;    // KeyCode: sign + or - (Keypad 's')
            var KC_QTY_1 = 81;     // KeyCode: Quantity (Keypad 'q')
            var KC_AMT_1 = 80;     // KeyCode: Price (Keypad 'p')
            var KC_DISC_1 = 68;    // KeyCode: Discount Percentage [0..100] (Keypad 'd')

            var KC_BACKSPACE = 8;  // KeyCode: Backspace (Keypad 'backspace')       
            var kc_lookup = {
                48: '0', 49: '1', 50: '2',  51: '3', 52: '4',
                53: '5', 54: '6', 55: '7', 56: '8', 57: '9',
                80: 'p', 83: 's', 68: 'd', 190: '.', 81: 'q',
                96: '0', 97: '1', 98: '2',  99: '3', 100: '4',
                101: '5', 102: '6', 103: '7', 104: '8', 105: '9',
                106: '*', 107: '+', 109: '-', 110: '.', 111: '/'
            };

            //usb keyboard keyup event
            var rx = /INPUT|SELECT|TEXTAREA/i;
            var ok = false;
            var timeStamp = 0;
            $('body').on('keyup', '', function (e){
                var statusHandler  =  !rx.test(e.target.tagName)  ||
                    e.target.disabled || e.target.readOnly;
                if (statusHandler){
                    var is_number = false;
                    var type = self.type;
                    var buttonMode = {
                        qty: 'quantity',
                        disc: 'discount',
                        price: 'price'
                    };
                    var token = e.keyCode;
                    if ((token >= 96 && token <= 105 || token == 110) ||
                        (token >= 48 && token <= 57 || token == 190)) {
                        self.data.type = type.numchar;
                        self.data.val = kc_lookup[token];
                        is_number = true;
                        ok = true;
                    }
                    else if (token == KC_PLU || token == KC_PLU_1) {
                        self.data.type = type.sign;
                        ok = true;
                    }
                    else if (token == KC_QTY || token == KC_QTY_1) {
                        self.data.type = type.bmode;
                        self.data.val = buttonMode.qty;
                        ok = true;
                    }
                    else if (token == KC_AMT || token == KC_AMT_1) {
                        self.data.type = type.bmode;
                        self.data.val = buttonMode.price;
                        ok = true;
                    }
                    else if (token == KC_DISC || token == KC_DISC_1) {
                        self.data.type = type.bmode;
                        self.data.val = buttonMode.disc;
                        ok = true;
                    }
                    else if (token == KC_BACKSPACE) {
                        self.data.type = type.backspace;
                        ok = true;
                    }

                    if (is_number) {
                        if (timeStamp + 50 > new Date().getTime()) {
                            ok = false;
                        }
                    }

                    timeStamp = new Date().getTime();

                    setTimeout(function(){
                        if (ok) {self.action_callback(self.data);}
                    }, 50);
                }
            });
        },

        // stops catching keyboard events 
        disconnect: function(){
            $('body').off('keyup', '')
        }
    });
    
    return {
        Keypad: Keypad
    };
});
Beispiel #10
0
odoo.define('stock.widgets', function (require) {
"use strict";

var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
var Model = require('web.Model');
var session = require('web.session');
var web_client = require('web.web_client');
var Widget = require('web.Widget');
var kanban_common = require('web_kanban.common');

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

// This widget makes sure that the scaling is disabled on mobile devices.
// Widgets that want to display fullscreen on mobile phone need to extend this
// widget.
var MobileWidget = Widget.extend({
    start: function(){
        if(!$('#oe-mobilewidget-viewport').length){
            $('head').append('<meta id="oe-mobilewidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
        }
        return this._super();
    },
    destroy: function(){
        $('#oe-mobilewidget-viewport').remove();
        return this._super();
    },
});

var PickingEditorWidget = Widget.extend({
    template: 'PickingEditorWidget',
    init: function(parent,options){
        this._super(parent,options);
        this.rows = [];
        this.search_filter = "";
    },
    get_header: function(){
        return this.getParent().get_header();
    },
    get_location: function(){
        var model = this.getParent();
        var locations = [];
        _.each(model.locations, function(loc){
            locations.push({name: loc.complete_name, id: loc.id,});
        });
        return locations;
    },
    get_logisticunit: function(){
        var model = this.getParent();
        var ul = [];
        _.each(model.uls, function(ulog){
            ul.push({name: ulog.name, id: ulog.id,});
        });
        return ul;
    },
    get_rows: function(){
        var model = this.getParent();
        this.rows = [];
        var self = this;
        var pack_created = [];
        _.each( model.packoplines, function(packopline){
                var pack = undefined;
                var color = "";
                if (packopline.product_id[1] !== undefined){ pack = packopline.package_id[1];}
                if (packopline.product_qty == packopline.qty_done){ color = "success "; }
                if (packopline.product_qty < packopline.qty_done){ color = "danger "; }
                //also check that we don't have a line already existing for that package
                if (packopline.result_package_id[1] !== undefined && $.inArray(packopline.result_package_id[0], pack_created) === -1){
                    var myPackage = $.grep(model.packages, function(e){ return e.id == packopline.result_package_id[0]; })[0];
                    self.rows.push({
                        cols: { product: packopline.result_package_id[1],
                                qty: '',
                                rem: '',
                                uom: undefined,
                                lot: undefined,
                                pack: undefined,
                                container: packopline.result_package_id[1],
                                container_id: undefined,
                                loc: packopline.location_id[1],
                                dest: packopline.location_dest_id[1],
                                id: packopline.result_package_id[0],
                                product_id: undefined,
                                can_scan: false,
                                head_container: true,
                                processed: packopline.processed,
                                package_id: myPackage.id,
                                ul_id: myPackage.ul_id[0],
                        },
                        classes: ('success container_head ') + (packopline.processed === "true" ? 'processed hidden ':''),
                    });
                    pack_created.push(packopline.result_package_id[0]);
                }
                self.rows.push({
                    cols: { product: packopline.product_id[1] || packopline.package_id[1],
                            qty: packopline.product_qty,
                            rem: packopline.qty_done,
                            uom: packopline.product_uom_id[1],
                            lot: packopline.lot_id[1],
                            pack: pack,
                            container: packopline.result_package_id[1],
                            container_id: packopline.result_package_id[0],
                            loc: packopline.location_id[1],
                            dest: packopline.location_dest_id[1],
                            id: packopline.id,
                            product_id: packopline.product_id[0],
                            can_scan: packopline.result_package_id[1] === undefined ? true : false,
                            head_container: false,
                            processed: packopline.processed,
                            package_id: undefined,
                            ul_id: -1,
                    },
                    classes: color + (packopline.result_package_id[1] !== undefined ? 'in_container_hidden ' : '') + (packopline.processed === "true" ? 'processed hidden ':''),
                });
        });
        //sort element by things to do, then things done, then grouped by packages
        var group_by_container = _.groupBy(self.rows, function(row){
            return row.cols.container;
        });
        var sorted_row = [];
        if (group_by_container.undefined !== undefined){
            group_by_container.undefined.sort(function(a,b){return (b.classes === '') - (a.classes === '');});
            $.each(group_by_container.undefined, function(key, value){
                sorted_row.push(value);
            });
        }

        $.each(group_by_container, function(key, value){
            if (key !== 'undefined'){
                $.each(value, function(k,v){
                    sorted_row.push(v);
                });
            }
        });

        return sorted_row;
    },
    renderElement: function(){
        var self = this;
        this._super();
        this.check_content_screen();
        this.$('.js_pick_done').click(function(){ self.getParent().done(); });
        this.$('.js_pick_print').click(function(){ self.getParent().print_picking(); });
        this.$('.oe_pick_app_header').text(self.get_header());
        this.$('.oe_searchbox').keyup(function(){
            self.on_searchbox($(this).val());
        });
        this.$('.js_putinpack').click(function(){ self.getParent().pack(); });
        this.$('.js_drop_down').click(function(){ self.getParent().drop_down();});
        this.$('.js_clear_search').click(function(){
            self.on_searchbox('');
            self.$('.oe_searchbox').val('');
        });
        this.$('.oe_searchbox').focus(function(){
            self.getParent().barcode_scanner.disconnect();
        });
        this.$('.oe_searchbox').blur(function(){
            self.getParent().barcode_scanner.connect(function(ean){
                self.get_Parent().scan(ean);
            });
        });
        this.$('#js_select').change(function(){
            var selection = self.$('#js_select option:selected').attr('value');
            if (selection === "ToDo"){
                self.getParent().$('.js_pick_pack').removeClass('hidden');
                self.getParent().$('.js_drop_down').removeClass('hidden');
                self.$('.js_pack_op_line.processed').addClass('hidden');
                self.$('.js_pack_op_line:not(.processed)').removeClass('hidden');
            }
            else{
                self.getParent().$('.js_pick_pack').addClass('hidden');
                self.getParent().$('.js_drop_down').addClass('hidden');
                self.$('.js_pack_op_line.processed').removeClass('hidden');
                self.$('.js_pack_op_line:not(.processed)').addClass('hidden');
            }
            self.on_searchbox(self.search_filter);
        });
        this.$('.js_plus').click(function(){
            var id = $(this).data('product-id');
            var op_id = $(this).parents("[data-id]:first").data('id');
            self.getParent().scan_product_id(id,1,op_id);
        });
        this.$('.js_minus').click(function(){
            var id = $(this).data('product-id');
            var op_id = $(this).parents("[data-id]:first").data('id');
            self.getParent().scan_product_id(id,-1,op_id);
        });
        this.$('.js_unfold').click(function(){
            var op_id = $(this).parent().data('id');
            var line = $(this).parent();
            //select all js_pack_op_line with class in_container_hidden and correct container-id
            var select = self.$('.js_pack_op_line.in_container_hidden[data-container-id='+op_id+']');
            if (select.length > 0){
                //we unfold
                line.addClass('warning');
                select.removeClass('in_container_hidden');
                select.addClass('in_container');
            }
            else{
                //we fold
                line.removeClass('warning');
                select = self.$('.js_pack_op_line.in_container[data-container-id='+op_id+']');
                select.removeClass('in_container');
                select.addClass('in_container_hidden');
            }
        });
        this.$('.js_create_lot').click(function(){
            var op_id = $(this).parents("[data-id]:first").data('id');
            var lot_name = false;
            self.$('.js_lot_scan').val('');
            var $lot_modal = self.$el.siblings('#js_LotChooseModal');
            //disconnect scanner to prevent scanning a product in the back while dialog is open
            self.getParent().barcode_scanner.disconnect();
            $lot_modal.modal();
            //focus input
            $lot_modal.on('shown.bs.modal', function(){
                self.$('.js_lot_scan').focus();
            });
            //reactivate scanner when dialog close
            $lot_modal.on('hidden.bs.modal', function(){
                self.getParent().barcode_scanner.connect(function(ean){
                    self.getParent().scan(ean);
                });
            });
            self.$('.js_lot_scan').focus();
            //button action
            self.$('.js_validate_lot').click(function(){
                //get content of input
                var name = self.$('.js_lot_scan').val();
                if (name.length !== 0){
                    lot_name = name;
                }
                $lot_modal.modal('hide');
                //we need this here since it is not sure the hide event
                //will be catch because we refresh the view after the create_lot call
                self.getParent().barcode_scanner.connect(function(ean){
                    self.getParent().scan(ean);
                });
                self.getParent().create_lot(op_id, lot_name);
            });
        });
        this.$('.js_delete_pack').click(function(){
            var pack_id = $(this).parents("[data-id]:first").data('id');
            self.getParent().delete_package_op(pack_id);
        });
        this.$('.js_print_pack').click(function(){
            var pack_id = $(this).parents("[data-id]:first").data('id');
            // $(this).parents("[data-id]:first").data('id')
            self.getParent().print_package(pack_id);
        });
        this.$('.js_submit_value').submit(function(){
            var op_id = $(this).parents("[data-id]:first").data('id');
            var value = parseFloat($("input", this).val());
            if (value>=0){
                self.getParent().set_operation_quantity(value, op_id);
            }
            $("input", this).val("");
            return false;
        });
        this.$('.js_qty').focus(function(){
            self.getParent().barcode_scanner.disconnect();
        });
        this.$('.js_qty').blur(function(){
            var op_id = $(this).parents("[data-id]:first").data('id');
            var value = parseFloat($(this).val());
            if (value>=0){
                self.getParent().set_operation_quantity(value, op_id);
            }
            
            self.getParent().barcode_scanner.connect(function(ean){
                self.getParent().scan(ean);
            });
        });
        this.$('.js_change_src').click(function(){
            var op_id = $(this).parents("[data-id]:first").data('id');//data('op_id');
            self.$('#js_loc_select').addClass('source');
            self.$('#js_loc_select').data('op-id',op_id);
            self.$el.siblings('#js_LocationChooseModal').modal();
        });
        this.$('.js_change_dst').click(function(){
            var op_id = $(this).parents("[data-id]:first").data('id');
            self.$('#js_loc_select').data('op-id',op_id);
            self.$el.siblings('#js_LocationChooseModal').modal();
        });
        this.$('.js_pack_change_dst').click(function(){
            var op_id = $(this).parents("[data-id]:first").data('id');
            self.$('#js_loc_select').addClass('pack');
            self.$('#js_loc_select').data('op-id',op_id);
            self.$el.siblings('#js_LocationChooseModal').modal();
        });
        this.$('.js_validate_location').click(function(){
            //get current selection
            var select_dom_element = self.$('#js_loc_select');
            var loc_id = self.$('#js_loc_select option:selected').data('loc-id');
            var src_dst = false;
            var op_id = select_dom_element.data('op-id');
            if (select_dom_element.hasClass('pack')){
                select_dom_element.removeClass('source');
                var op_ids = [];
                self.$('.js_pack_op_line[data-container-id='+op_id+']').each(function(){
                    op_ids.push($(this).data('id'));
                });
                op_id = op_ids;
            }
            else if (select_dom_element.hasClass('source')){
                src_dst = true;
                select_dom_element.removeClass('source');
            }
            if (loc_id === false){
                //close window
                self.$el.siblings('#js_LocationChooseModal').modal('hide');
            }
            else{
                self.$el.siblings('#js_LocationChooseModal').modal('hide');
                self.getParent().change_location(op_id, parseInt(loc_id), src_dst);

            }
        });
        this.$('.js_pack_configure').click(function(){
            var pack_id = $(this).parents(".js_pack_op_line:first").data('package-id');
            var ul_id = $(this).parents(".js_pack_op_line:first").data('ulid');
            self.$('#js_packconf_select').val(ul_id);
            self.$('#js_packconf_select').data('pack-id',pack_id);
            self.$el.siblings('#js_PackConfModal').modal();
        });
        this.$('.js_validate_pack').click(function(){
            //get current selection
            var select_dom_element = self.$('#js_packconf_select');
            var ul_id = self.$('#js_packconf_select option:selected').data('ul-id');
            var pack_id = select_dom_element.data('pack-id');
            self.$el.siblings('#js_PackConfModal').modal('hide');
            if (pack_id){
                self.getParent().set_package_pack(pack_id, ul_id);
                $('.container_head[data-package-id="'+pack_id+'"]').data('ulid', ul_id);
            }
        });
        
        //remove navigation bar from default openerp GUI
        $('td.navbar').html('<div></div>');
    },
    on_searchbox: function(query){
        //hide line that has no location matching the query and highlight location that match the query
        this.search_filter = query;
        var processed = ".processed";
        if (this.$('#js_select option:selected').attr('value') == "ToDo"){
            processed = ":not(.processed)";
        }
        if (query !== '') {
            this.$('.js_loc:not(.js_loc:Contains('+query+'))').removeClass('info');
            this.$('.js_loc:Contains('+query+')').addClass('info');
            this.$('.js_pack_op_line'+processed+':not(.js_pack_op_line:has(.js_loc:Contains('+query+')))').addClass('hidden');
            this.$('.js_pack_op_line'+processed+':has(.js_loc:Contains('+query+'))').removeClass('hidden');
        }
        //if no query specified, then show everything
        if (query === '') {
            this.$('.js_loc').removeClass('info');
            this.$('.js_pack_op_line'+processed+'.hidden').removeClass('hidden');
        }
        this.check_content_screen();
    },
    check_content_screen: function(){
        //get all visible element and if none has positive qty, disable put in pack and process button
        var self = this;
        var processed = this.$('.js_pack_op_line.processed');
        var qties = this.$('.js_pack_op_line:not(.processed):not(.hidden) .js_qty').map(function(){return $(this).val();});
        var container = this.$('.js_pack_op_line.container_head:not(.processed):not(.hidden)');
        var disabled = true;
        $.each(qties,function(index, value){
            if(value>0) {
                disabled = false;
            }
        });

        if (disabled){
            if (container.length===0){
                self.$('.js_drop_down').addClass('disabled');
            }
            else {
                self.$('.js_drop_down').removeClass('disabled');
            }
            self.$('.js_pick_pack').addClass('disabled');
            if (processed.length === 0){
                self.$('.js_pick_done').addClass('disabled');
            }
            else {
                self.$('.js_pick_done').removeClass('disabled');
            }
        }
        else{
            self.$('.js_drop_down').removeClass('disabled');
            self.$('.js_pick_pack').removeClass('disabled');
            self.$('.js_pick_done').removeClass('disabled');
        }
    },
    get_current_op_selection: function(ignore_container){
        //get ids of visible on the screen
        var pack_op_ids = [];
        this.$('.js_pack_op_line:not(.processed):not(.js_pack_op_line.hidden):not(.container_head)').each(function(){
            var cur_id = $(this).data('id');
            pack_op_ids.push(parseInt(cur_id));
        });
        //get list of element in this.rows where rem > 0 and container is empty is specified
        var list = [];
        _.each(this.rows, function(row){
            if (row.cols.rem > 0 && (ignore_container || row.cols.container === undefined)){
                list.push(row.cols.id);
            }
        });
        //return only those visible with rem qty > 0 and container empty
        return _.intersection(pack_op_ids, list);
    },
    remove_blink: function(){
        this.$('.js_pack_op_line.blink_me').removeClass('blink_me');
    },
    blink: function(op_id){
        this.$('.js_pack_op_line[data-id="'+op_id+'"]').addClass('blink_me');
    },
    check_done: function(){
        var model = this.getParent();
        var done = true;
        _.each( model.packoplines, function(packopline){
            if (packopline.processed === "false"){
                done = false;
                return done;
            }
        });
        return done;
    },
    get_visible_ids: function(){
        var visible_op_ids = [];
        var op_ids = this.$('.js_pack_op_line:not(.processed):not(.hidden):not(.container_head):not(.in_container):not(.in_container_hidden)').map(function(){
            return $(this).data('id');
        });
        $.each(op_ids, function(key, op_id){
            visible_op_ids.push(parseInt(op_id));
        });
        return visible_op_ids;
    },
});

var PickingMenuWidget = MobileWidget.extend({
    template: 'PickingMenuWidget',
    init: function(parent, params){
        this._super(parent,params);
        var self = this;
        $(window).bind('hashchange', function(){
            var states = $.bbq.getState();
            if (states.action === "stock.ui"){
                self.do_action({
                    type:   'ir.actions.client',
                    tag:    'stock.ui',
                    target: 'current',
                },{
                    clear_breadcrumbs: true,
                });
            }
        });
        this.picking_types = [];
        this.loaded = this.load();
        this.scanning_type = 0;
        this.barcode_scanner = new BarcodeScanner();
        this.pickings_by_type = {};
        this.pickings_by_id = {};
        this.picking_search_string = "";
    },
    load: function(){
        var self = this;
        return new Model('stock.picking.type').get_func('search_read')([],[])
            .then(function(types){
                self.picking_types = types;
                var type_ids = [];
                for(var i = 0; i < types.length; i++){
                    self.pickings_by_type[types[i].id] = [];
                    type_ids.push(types[i].id);
                }
                self.pickings_by_type[0] = [];

                return new Model('stock.picking').call('search_read',[ [['state','in', ['assigned', 'partially_available']], ['picking_type_id', 'in', type_ids]], [] ], {context: new data.CompoundContext()});

            }).then(function(pickings){
                self.pickings = pickings;
                for(var i = 0; i < pickings.length; i++){
                    var picking = pickings[i];
                    self.pickings_by_type[picking.picking_type_id[0]].push(picking);
                    self.pickings_by_id[picking.id] = picking;
                    self.picking_search_string += '' + picking.id + ':' + (picking.name ? picking.name.toUpperCase(): '') + '\n';
                }

            });
    },
    renderElement: function(){
        this._super();
        var self = this;
        this.$('.js_pick_quit').click(function(){ self.quit(); });
        this.$('.js_pick_scan').click(function(){ self.scan_picking($(this).data('id')); });
        this.$('.js_pick_last').click(function(){ self.goto_last_picking_of_type($(this).data('id')); });
        this.$('.oe_searchbox').keyup(function(){
            self.on_searchbox($(this).val());
        });
        //remove navigation bar from default openerp GUI
        $('td.navbar').html('<div></div>');
    },
    start: function(){
        this._super();
        var self = this;
        web_client.set_content_full_screen(true);
        this.barcode_scanner.connect(function(barcode){
            self.on_scan(barcode);
        });
        this.loaded.then(function(){
            self.renderElement();
        });
    },
    goto_picking: function(picking_id){
        $.bbq.pushState('#action=stock.ui&picking_id='+picking_id);
        $(window).trigger('hashchange');
    },
    goto_last_picking_of_type: function(type_id){
        $.bbq.pushState('#action=stock.ui&picking_type_id='+type_id);
        $(window).trigger('hashchange');
    },
    search_picking: function(barcode){
        try {
            var re = RegExp("([0-9]+):.*?"+barcode.toUpperCase(),"gi");
        }
        catch(e) {
            //avoid crash if a not supported char is given (like '\' or ')')
        return [];
        }

        var results = [];
        var r;
        for(var i = 0; i < 100; i++){
            r = re.exec(this.picking_search_string);
            if(r){
                var picking = this.pickings_by_id[Number(r[1])];
                if(picking){
                    results.push(picking);
                }
            }else{
                break;
            }
        }
        return results;
    },
    on_scan: function(barcode){
        var self = this;
        for(var i = 0, len = this.pickings.length; i < len; i++){
            var picking = this.pickings[i];
            if(picking.name.toUpperCase() === $.trim(barcode.toUpperCase())){
                this.goto_picking(picking.id);
                break;
            }
        }
        this.$('.js_picking_not_found').removeClass('hidden');

        clearTimeout(this.picking_not_found_timeout);
        this.picking_not_found_timeout = setTimeout(function(){
            self.$('.js_picking_not_found').addClass('hidden');
        },2000);

    },
    on_searchbox: function(query){
        var self = this;

        clearTimeout(this.searchbox_timeout);
        this.searchbox_timout = setTimeout(function(){
            if(query){
                self.$('.js_picking_not_found').addClass('hidden');
                self.$('.js_picking_categories').addClass('hidden');
                self.$('.js_picking_search_results').html(
                    QWeb.render('PickingSearchResults',{results:self.search_picking(query)})
                );
                self.$('.js_picking_search_results .oe_picking').click(function(){
                    self.goto_picking($(this).data('id'));
                });
                self.$('.js_picking_search_results').removeClass('hidden');
            }else{
                self.$('.js_title_label').removeClass('hidden');
                self.$('.js_picking_categories').removeClass('hidden');
                self.$('.js_picking_search_results').addClass('hidden');
            }
        },100);
    },
    quit: function(){
        return new Model("ir.model.data").get_func("search_read")([['name', '=', 'action_picking_type_form']], ['res_id']).pipe(function(res) {
                window.location = '/web#action=' + res[0]['res_id'];
            });
    },
    destroy: function(){
        this._super();
        this.barcode_scanner.disconnect();
        web_client.set_content_full_screen(false);
    },
});
core.action_registry.add('stock.menu', PickingMenuWidget);

var PickingMainWidget = MobileWidget.extend({
    template: 'PickingMainWidget',
    init: function(parent,params){
        this._super(parent,params);
        var self = this;
        $(window).bind('hashchange', function(){
            var states = $.bbq.getState();
            if (states.action === "stock.menu"){
                self.do_action({
                    type:   'ir.actions.client',
                    tag:    'stock.menu',
                    target: 'current',
                },{
                    clear_breadcrumbs: true,
                });
            }
        });
        var init_hash = $.bbq.getState();
        this.picking_type_id = init_hash.picking_type_id ? init_hash.picking_type_id:0;
        this.picking_id = init_hash.picking_id ? init_hash.picking_id:undefined;
        this.picking = null;
        this.pickings = [];
        this.packoplines = null;
        this.selected_operation = { id: null, picking_id: null};
        this.packages = null;
        this.barcode_scanner = new BarcodeScanner();
        this.locations = [];
        this.uls = [];
        if(this.picking_id){
            this.loaded =  this.load(this.picking_id);
        }else{
            this.loaded =  this.load();
        }

    },

    // load the picking data from the server. If picking_id is undefined, it will take the first picking
    // belonging to the category
    load: function(picking_id){
        var self = this;
        var loaded_picking;

        function load_picking_list(type_id){
            var pickings = new $.Deferred();
            new Model('stock.picking')
                .call('get_next_picking_for_ui',[{'default_picking_type_id':parseInt(type_id)}])
                .then(function(picking_ids){
                    if(!picking_ids || picking_ids.length === 0){
                        (new Dialog(self,{
                            title: _t('No Picking Available'),
                            buttons: [{
                                text:_t('Ok'),
                                click: function(){
                                    self.menu();
                                }
                            }]
                        }, _t('<p>We could not find a picking to display.</p>'))).open();

                        pickings.reject();
                    }else{
                        self.pickings = picking_ids;
                        pickings.resolve(picking_ids);
                    }
                });

            return pickings;
        }

        // if we have a specified picking id, we load that one, and we load the picking of the same type as the active list
        if( picking_id ){
            loaded_picking = new Model('stock.picking')
                .call('read',[[parseInt(picking_id)], [], new data.CompoundContext()])
                .then(function(picking){
                    self.picking = picking[0];
                    self.picking_type_id = picking[0].picking_type_id[0];
                    return load_picking_list(self.picking.picking_type_id[0]);
                });
        }else{
            // if we don't have a specified picking id, we load the pickings belong to the specified type, and then we take
            // the first one of that list as the active picking
            loaded_picking = new $.Deferred();
            load_picking_list(self.picking_type_id)
                .then(function(){
                    return new Model('stock.picking').call('read',[self.pickings[0],[], new data.CompoundContext()]);
                })
                .then(function(picking){
                    self.picking = picking;
                    self.picking_type_id = picking.picking_type_id[0];
                    loaded_picking.resolve();
                });
        }

        return loaded_picking.then(function(){
                return new Model('stock.location').call('search',[[['usage','=','internal']]]).then(function(locations_ids){
                    return new Model('stock.location').call('read',[locations_ids, []]).then(function(locations){
                        self.locations = locations;
                    });
                });
            }).then(function(){
                return new Model('stock.picking').call('check_group_pack').then(function(result){
                    self.show_pack = result;
                    return result;
                });
            }).then(function(){
                return new Model('stock.picking').call('check_group_lot').then(function(result){
                    self.show_lot = result;
                    return result;
                });
            }).then(function(){
                if (self.picking.pack_operation_exist === false){
                    self.picking.recompute_pack_op = false;
                    return new Model('stock.picking').call('do_prepare_partial',[[self.picking.id]]);
                }
            }).then(function(){
                    return new Model('stock.pack.operation').call('search',[[['picking_id','=',self.picking.id]]]);
            }).then(function(pack_op_ids){
                    return new Model('stock.pack.operation').call('read',[pack_op_ids, [], new data.CompoundContext()]);
            }).then(function(operations){
                self.packoplines = operations;
                var package_ids = [];

                for(var i = 0; i < operations.length; i++){
                    if(!_.contains(package_ids,operations[i].result_package_id[0])){
                        if (operations[i].result_package_id[0]){
                            package_ids.push(operations[i].result_package_id[0]);
                        }
                    }
                }
                return new Model('stock.quant.package').call('read',[package_ids, [], new data.CompoundContext()]);
            }).then(function(packages){
                self.packages = packages;
            }).then(function(){
                    return new Model('product.ul').call('search',[[]]);
            }).then(function(uls_ids){
                    return new Model('product.ul').call('read',[uls_ids, []]);
            }).then(function(uls){
                self.uls = uls;
            });
    },
    start: function(){
        // this._super();
        var self = this;
        web_client.set_content_full_screen(true);
        this.barcode_scanner.connect(function(ean){
            self.scan(ean);
        });

        this.$('.js_pick_quit').click(function(){ self.quit(); });
        this.$('.js_pick_prev').click(function(){ self.picking_prev(); });
        this.$('.js_pick_next').click(function(){ self.picking_next(); });
        this.$('.js_pick_menu').click(function(){ self.menu(); });
        this.$('.js_reload_op').click(function(){ self.reload_pack_operation();});

        return $.when(this._super(), this.loaded).done(function(){
            self.picking_editor = new PickingEditorWidget(self);
            self.picking_editor.replace(self.$('.oe_placeholder_picking_editor'));

            if( self.picking.id === self.pickings[0]){
                self.$('.js_pick_prev').addClass('disabled');
            }else{
                self.$('.js_pick_prev').removeClass('disabled');
            }

            if( self.picking.id === self.pickings[self.pickings.length-1] ){
                self.$('.js_pick_next').addClass('disabled');
            }else{
                self.$('.js_pick_next').removeClass('disabled');
            }
            if (self.picking.recompute_pack_op){
                self.$('.oe_reload_op').removeClass('hidden');
            }
            else {
                self.$('.oe_reload_op').addClass('hidden');
            }
            if (!self.show_pack){
                self.$('.js_pick_pack').addClass('hidden');
            }
            if (!self.show_lot){
                self.$('.js_create_lot').addClass('hidden');
            }

        }).fail(function(error) {console.log(error);});

    },
    on_searchbox: function(query){
        var self = this;
        self.picking_editor.on_searchbox(query.toUpperCase());
    },
    // reloads the data from the provided picking and refresh the ui.
    // (if no picking_id is provided, gets the first picking in the db)
    refresh_ui: function(picking_id){
        var self = this;
        var remove_search_filter = "";
        if (self.picking.id === picking_id){
            remove_search_filter = self.$('.oe_searchbox').val();
        }
        return this.load(picking_id)
            .then(function(){
                self.picking_editor.remove_blink();
                self.picking_editor.renderElement();
                if (!self.show_pack){
                    self.$('.js_pick_pack').addClass('hidden');
                }
                if (!self.show_lot){
                    self.$('.js_create_lot').addClass('hidden');
                }
                if (self.picking.recompute_pack_op){
                    self.$('.oe_reload_op').removeClass('hidden');
                }
                else {
                    self.$('.oe_reload_op').addClass('hidden');
                }

                if( self.picking.id === self.pickings[0]){
                    self.$('.js_pick_prev').addClass('disabled');
                }else{
                    self.$('.js_pick_prev').removeClass('disabled');
                }

                if( self.picking.id === self.pickings[self.pickings.length-1] ){
                    self.$('.js_pick_next').addClass('disabled');
                }else{
                    self.$('.js_pick_next').removeClass('disabled');
                }
                if (remove_search_filter === ""){
                    self.$('.oe_searchbox').val('');
                    self.on_searchbox('');
                }
                else{
                    self.$('.oe_searchbox').val(remove_search_filter);
                    self.on_searchbox(remove_search_filter);
                }
            });
    },
    get_header: function(){
        if(this.picking){
            return this.picking.name;
        }else{
            return '';
        }
    },
    menu: function(){
        $.bbq.pushState('#action=stock.menu');
        $(window).trigger('hashchange');
    },
    scan: function(ean){ //scans a barcode, sends it to the server, then reload the ui
        var self = this;
        var product_visible_ids = this.picking_editor.get_visible_ids();
        return new Model('stock.picking')
            .call('process_barcode_from_ui', [self.picking.id, ean, product_visible_ids])
            .then(function(result){
                if (result.filter_loc !== false){
                    //check if we have receive a location as answer
                    if (result.filter_loc !== undefined){
                        var modal_loc_hidden = self.$('#js_LocationChooseModal').attr('aria-hidden');
                        if (modal_loc_hidden === "false"){
                            self.$('#js_LocationChooseModal .js_loc_option[data-loc-id='+result.filter_loc_id+']').attr('selected','selected');
                        }
                        else{
                            self.$('.oe_searchbox').val(result.filter_loc);
                            self.on_searchbox(result.filter_loc);
                        }
                    }
                }
                if (result.operation_id !== false){
                    self.refresh_ui(self.picking.id).then(function(){
                        return self.picking_editor.blink(result.operation_id);
                    });
                }
            });
    },
    scan_product_id: function(product_id,increment,op_id){ //performs the same operation as a scan, but with product id instead, increment is the value to increment (-1 or 1)
        var self = this;
        return new Model('stock.picking')
            .call('process_product_id_from_ui', [self.picking.id, product_id, op_id, increment])
            .then(function(){
                return self.refresh_ui(self.picking.id);
            });
    },
    pack: function(){
        var self = this;
        var pack_op_ids = self.picking_editor.get_current_op_selection(false);
        if (pack_op_ids.length !== 0){
            return new Model('stock.picking')
                .call('action_pack',[[[self.picking.id]], pack_op_ids])
                .then(function(){
                    //TODO: the functionality using current_package_id in context is not needed anymore
                    session.user_context.current_package_id = false;
                    return self.refresh_ui(self.picking.id);
                });
        }
    },
    drop_down: function(){
        var self = this;
        var pack_op_ids = self.picking_editor.get_current_op_selection(true);
        if (pack_op_ids.length !== 0){
            return new Model('stock.pack.operation')
                .call('action_drop_down', [pack_op_ids])
                .then(function(){
                        return self.refresh_ui(self.picking.id).then(function(){
                            if (self.picking_editor.check_done()){
                                return self.done();
                            }
                        });
                });
        }
    },
    done: function(){
        var self = this;
        return new Model('stock.picking')
            .call('action_done_from_ui',[self.picking.id, {'default_picking_type_id': self.picking_type_id}])
            .then(function(new_picking_ids){
                if (new_picking_ids){
                    return self.refresh_ui(new_picking_ids[0]);
                }
                else {
                    return 0;
                }
            });
    },
    create_lot: function(op_id, lot_name){
        var self = this;
        return new Model('stock.pack.operation')
            .call('create_and_assign_lot',[parseInt(op_id), lot_name])
            .then(function(){
                return self.refresh_ui(self.picking.id);
            });
    },
    change_location: function(op_id, loc_id, is_src_dst){
        var self = this;
        var vals = {'location_dest_id': loc_id};
        if (is_src_dst){
            vals = {'location_id': loc_id};
        }
        return new Model('stock.pack.operation')
            .call('write',[op_id, vals])
            .then(function(){
                return self.refresh_ui(self.picking.id);
            });
    },
    print_package: function(package_id){
        var self = this;
        return new Model('stock.quant.package')
            .call('action_print',[[package_id]])
            .then(function(action){
                return self.do_action(action);
            });
    },
    print_picking: function(){
        var self = this;
        return new Model('stock.picking.type').call('read', [[self.picking_type_id], ['code'], new data.CompoundContext()])
            .then(function(){
                return new Model('stock.picking').call('do_print_picking',[[self.picking.id]])
                       .then(function(action){
                            return self.do_action(action);
                       });
            });
    },
    picking_next: function(){
        for(var i = 0; i < this.pickings.length; i++){
            if(this.pickings[i] === this.picking.id){
                if(i < this.pickings.length -1){
                    $.bbq.pushState('picking_id='+this.pickings[i+1]);
                    this.refresh_ui(this.pickings[i+1]);
                    return;
                }
            }
        }
    },
    picking_prev: function(){
        for(var i = 0; i < this.pickings.length; i++){
            if(this.pickings[i] === this.picking.id){
                if(i > 0){
                    $.bbq.pushState('picking_id='+this.pickings[i-1]);
                    this.refresh_ui(this.pickings[i-1]);
                    return;
                }
            }
        }
    },
    delete_package_op: function(pack_id){
        var self = this;
        return new Model('stock.pack.operation').call('search', [[['result_package_id', '=', pack_id]]])
            .then(function(op_ids) {
                return new Model('stock.pack.operation').call('write', [op_ids, {'result_package_id':false}])
                    .then(function() {
                        return self.refresh_ui(self.picking.id);
                    });
            });
    },
    set_operation_quantity: function(quantity, op_id){
        var self = this;
        if(quantity >= 0){
            return new Model('stock.pack.operation')
                .call('write',[[op_id],{'qty_done': quantity }])
                .then(function(){
                    self.refresh_ui(self.picking.id);
                });
        }

    },
    set_package_pack: function(package_id, pack){
        return new Model('stock.quant.package')
            .call('write',[[package_id],{'ul_id': pack }]);
    },
    reload_pack_operation: function(){
        var self = this;
        return new Model('stock.picking')
            .call('do_prepare_partial',[[self.picking.id]])
            .then(function(){
                self.refresh_ui(self.picking.id);
            });
    },
    quit: function(){
        this.destroy();
        return new Model("ir.model.data").get_func("search_read")([['name', '=', 'action_picking_type_form']], ['res_id']).pipe(function(res) {
                window.location = '/web#action=' + res[0]['res_id'];
            });
    },
    destroy: function(){
        this._super();
        // this.disconnect_numpad();
        this.barcode_scanner.disconnect();
        web_client.set_content_full_screen(false);
    },
});
core.action_registry.add('stock.ui', PickingMainWidget);

var BarcodeScanner = core.Class.extend({
    connect: function(callback){
        var code = "";
        var timeStamp = 0;
        var timeout = null;

        this.handler = function(e){
            if(e.which === 13){ //ignore returns
                return;
            }

            if(timeStamp + 50 < new Date().getTime()){
                code = "";
            }

            timeStamp = new Date().getTime();
            clearTimeout(timeout);

            code += String.fromCharCode(e.which);

            timeout = setTimeout(function(){
                if(code.length >= 3){
                    callback(code);
                }
                code = "";
            },100);
        };

        $('body').on('keypress', this.handler);

    },
    disconnect: function(){
        $('body').off('keypress', this.handler);
    },
});

kanban_common.KanbanRecord.include({
    on_card_clicked: function() {
        if (this.view.dataset.model === 'stock.picking.type') {
            this.$('.oe_kanban_stock_picking_type_list').first().click();
        } else {
            this._super.apply(this, arguments);
        }
    },
});

});
Beispiel #11
0
odoo.define('barcodes.BarcodeEvents', function(require) {
"use strict";

var core = require('web.core');
var mixins = require('web.mixins');
var session = require('web.session');


// For IE >= 9, use this, new CustomEvent(), instead of new Event()
function CustomEvent ( event, params ) {
    params = params || { bubbles: false, cancelable: false, detail: undefined };
    var evt = document.createEvent( 'CustomEvent' );
    evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
    return evt;
   }
CustomEvent.prototype = window.Event.prototype;

var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
    timeout: null,
    key_pressed: {},
    buffered_key_events: [],
    // Regexp to match a barcode input and extract its payload
    // Note: to build in init() if prefix/suffix can be configured
    regexp: /(.{3,})[\n\r\t]*/,
    // By knowing the terminal character we can interpret buffered keys
    // as a barcode as soon as it's encountered (instead of waiting x ms)
    suffix: /[\n\r\t]+/,
    // Keys from a barcode scanner are usually processed as quick as possible,
    // but some scanners can use an intercharacter delay (we support <= 50 ms)
    max_time_between_keys_in_ms: session.max_time_between_keys_in_ms || 55,
    // To be able to receive the barcode value, an input must be focused.
    // On mobile devices, this causes the virtual keyboard to open.
    // Unfortunately it is not possible to avoid this behavior...
    // To avoid keyboard flickering at each detection of a barcode value,
    // we want to keep it open for a while (800 ms).
    inputTimeOut: 800,

    init: function() {
        mixins.PropertiesMixin.init.call(this);
        // Keep a reference of the handler functions to use when adding and removing event listeners
        this.__keydown_handler = _.bind(this.keydown_handler, this);
        this.__keyup_handler = _.bind(this.keyup_handler, this);
        this.__handler = _.bind(this.handler, this);
        // Bind event handler once the DOM is loaded
        // TODO: find a way to be active only when there are listeners on the bus
        $(_.bind(this.start, this, false));

        // Mobile device detection
        var isMobile = navigator.userAgent.match(/Android/i) ||
                       navigator.userAgent.match(/webOS/i) ||
                       navigator.userAgent.match(/iPhone/i) ||
                       navigator.userAgent.match(/iPad/i) ||
                       navigator.userAgent.match(/iPod/i) ||
                       navigator.userAgent.match(/BlackBerry/i) ||
                       navigator.userAgent.match(/Windows Phone/i);
        this.isChromeMobile = isMobile && navigator.userAgent.match(/Chrome/i);

        // Creates an input who will receive the barcode scanner value.
        if (this.isChromeMobile) {
            this.$barcodeInput = $('<input/>', {
                name: 'barcode',
                type: 'text',
                css: {
                    'position': 'fixed',
                    'top': '50%',
                    'transform': 'translateY(-50%)',
                    'z-index': '-1',
                },
            });
            // Avoid to show autocomplete for a non appearing input
            this.$barcodeInput.attr('autocomplete', 'off');
        }

        this.__blurBarcodeInput = _.debounce(this._blurBarcodeInput, this.inputTimeOut);
    },

    handle_buffered_keys: function() {
        var str = this.buffered_key_events.reduce(function(memo, e) { return memo + String.fromCharCode(e.which) }, '');
        var match = str.match(this.regexp);

        if (match) {
            var barcode = match[1];

            // Send the target in case there are several barcode widgets on the same page (e.g.
            // registering the lot numbers in a stock picking)
            core.bus.trigger('barcode_scanned', barcode, this.buffered_key_events[0].target);

            // Dispatch a barcode_scanned DOM event to elements that have barcode_events="true" set.
            if (this.buffered_key_events[0].target.getAttribute("barcode_events") === "true")
                $(this.buffered_key_events[0].target).trigger('barcode_scanned', barcode);
        } else {
            this.resend_buffered_keys();
        }

        this.buffered_key_events = [];
    },

    resend_buffered_keys: function() {
        var old_event, new_event;
        for(var i = 0; i < this.buffered_key_events.length; i++) {
            old_event = this.buffered_key_events[i];

            if(old_event.which !== 13) { // ignore returns
                // We do not create a 'real' keypress event through
                // eg. KeyboardEvent because there are several issues
                // with them that make them very different from
                // genuine keypresses. Chrome per example has had a
                // bug for the longest time that causes keyCode and
                // charCode to not be set for events created this way:
                // https://bugs.webkit.org/show_bug.cgi?id=16735
                var params = {
                    'bubbles': old_event.bubbles,
                    'cancelable': old_event.cancelable,
                };
                new_event = $.Event('keypress', params);
                new_event.viewArg = old_event.viewArg;
                new_event.ctrl = old_event.ctrl;
                new_event.alt = old_event.alt;
                new_event.shift = old_event.shift;
                new_event.meta = old_event.meta;
                new_event.char = old_event.char;
                new_event.key = old_event.key;
                new_event.charCode = old_event.charCode;
                new_event.keyCode = old_event.keyCode || old_event.which; // Firefox doesn't set keyCode for keypresses, only keyup/down
                new_event.which = old_event.which;
                new_event.dispatched_by_barcode_reader = true;

                $(old_event.target).trigger(new_event);
            }
        }
    },

    element_is_editable: function(element) {
        return $(element).is('input,textarea,[contenteditable="true"]');
    },

    // This checks that a keypress event is either ESC, TAB, an arrow
    // key or a function key. This is Firefox specific, in Chrom{e,ium}
    // keypress events are not fired for these types of keys, only
    // keyup/keydown.
    is_special_key: function(e) {
        if (e.key === "ArrowLeft" || e.key === "ArrowRight" ||
            e.key === "ArrowUp" || e.key === "ArrowDown" ||
            e.key === "Escape" || e.key === "Tab" ||
            e.key === "Backspace" || e.key === "Delete" ||
            e.key === "Home" || e.key === "End" ||
            e.key === "PageUp" || e.key === "PageDown" ||
            e.key === "Unidentified" || /F\d\d?/.test(e.key)) {
            return true;
        } else {
            return false;
        }
    },

    // The keydown and keyup handlers are here to disallow key
    // repeat. When preventDefault() is called on a keydown event
    // the keypress that normally follows is cancelled.
    keydown_handler: function(e){
        if (this.key_pressed[e.which]) {
            e.preventDefault();
        } else {
            this.key_pressed[e.which] = true;
        }
    },

    keyup_handler: function(e){
        this.key_pressed[e.which] = false;
    },

    handler: function(e){
        // Don't catch events we resent
        if (e.dispatched_by_barcode_reader)
            return;
        // Don't catch non-printable keys for which Firefox triggers a keypress
        if (this.is_special_key(e))
            return;
        // Don't catch keypresses which could have a UX purpose (like shortcuts)
        if (e.ctrlKey || e.metaKey || e.altKey)
            return;
        // Don't catch Return when nothing is buffered. This way users
        // can still use Return to 'click' on focused buttons or links.
        if (e.which === 13 && this.buffered_key_events.length === 0)
            return;
        // Don't catch events targeting elements that are editable because we
        // have no way of redispatching 'genuine' key events. Resent events
        // don't trigger native event handlers of elements. So this means that
        // our fake events will not appear in eg. an <input> element.
        if ((this.element_is_editable(e.target) && !$(e.target).data('enableBarcode')) && e.target.getAttribute("barcode_events") !== "true")
            return;

        // Catch and buffer the event
        this.buffered_key_events.push(e);
        e.preventDefault();
        e.stopImmediatePropagation();

        // Handle buffered keys immediately if the the keypress marks the end
        // of a barcode or after x milliseconds without a new keypress
        clearTimeout(this.timeout);
        if (String.fromCharCode(e.which).match(this.suffix)) {
            this.handle_buffered_keys();
        } else {
            this.timeout = setTimeout(_.bind(this.handle_buffered_keys, this), this.max_time_between_keys_in_ms);
        }
    },

    /**
     * Try to detect the barcode value by listening all keydown events:
     * Checks if a dom element who may contains text value has the focus.
     * If not, it's probably because these events are triggered by a barcode scanner.
     * To be able to handle this value, a focused input will be created.
     *
     * This function also has the responsibility to detect the end of the barcode value.
     * (1) In most of cases, an optional key (tab or enter) is sent to mark the end of the value.
     * So, we direclty handle the value.
     * (2) If no end key is configured, we have to calculate the delay between each keydowns.
     * 'max_time_between_keys_in_ms' depends of the device and may be configured.
     * Exceeded this timeout, we consider that the barcode value is entirely sent.
     *
     * @private
     * @param  {jQuery.Event} e keydown event
     */
    _listenBarcodeScanner: function (e) {
        if (!$('input:text:focus, textarea:focus, [contenteditable]:focus').length) {
            $('body').append(this.$barcodeInput);
            this.$barcodeInput.focus();
        }
        if (this.$barcodeInput.is(":focus")) {
            // Handle buffered keys immediately if the keypress marks the end
            // of a barcode or after x milliseconds without a new keypress.
            clearTimeout(this.timeout);
            // On chrome mobile, e.which only works for some special characters like ENTER or TAB.
            if (String.fromCharCode(e.which).match(this.suffix)) {
                this._handleBarcodeValue(e);
            } else {
                this.timeout = setTimeout(this._handleBarcodeValue.bind(this, e),
                    this.max_time_between_keys_in_ms);
            }
            // if the barcode input doesn't receive keydown for a while, remove it.
            this.__blurBarcodeInput();
        }
    },

    /**
     * Retrieves the barcode value from the temporary input element.
     * This checks this value and trigger it on the bus.
     *
     * @private
     * @param  {jQuery.Event} keydown event
     */
    _handleBarcodeValue: function (e) {
        var barcodeValue = this.$barcodeInput.val();
        if (barcodeValue.match(this.regexp)) {
            core.bus.trigger('barcode_scanned', barcodeValue, $(e.target).parent()[0]);
            this._blurBarcodeInput();
        }
    },

    /**
     * Removes the value and focus from the barcode input.
     * If nothing happens, the focus will be lost and
     * the virtual keyboard on mobile devices will be closed.
     *
     * @private
     */
    _blurBarcodeInput: function () {
        if (this.$barcodeInput) {
            // Close the virtual keyboard on mobile browsers
            // FIXME: actually we can't prevent keyboard from opening
            this.$barcodeInput.val('').blur();
        }
    },

    start: function(prevent_key_repeat){
        // Chrome Mobile isn't triggering keypress event.
        // This is marked as Legacy in the DOM-Level-3 Standard.
        // See: https://www.w3.org/TR/uievents/#legacy-keyboardevent-event-types
        // This fix is only applied for Google Chrome Mobile but it should work for
        // all other cases.
        // In master, we could remove the behavior with keypress and only use keydown.
        if (this.isChromeMobile) {
            $('body').on("keydown", this._listenBarcodeScanner.bind(this));
        } else {
            $('body').bind("keypress", this.__handler);
        }
        if (prevent_key_repeat === true) {
            $('body').bind("keydown", this.__keydown_handler);
            $('body').bind('keyup', this.__keyup_handler);
        }
    },

    stop: function(){
        $('body').unbind("keypress", this.__handler);
        $('body').unbind("keydown", this.__keydown_handler);
        $('body').unbind('keyup', this.__keyup_handler);
    },
});

return {
    /** Singleton that emits barcode_scanned events on core.bus */
    BarcodeEvents: new BarcodeEvents(),
    /**
     * List of barcode prefixes that are reserved for internal purposes
     * @type Array
     */
    ReservedBarcodePrefixes: ['O-CMD'],
};

});
Beispiel #12
0
odoo.define('web_tour.TourManager', function(require) {
"use strict";

var core = require('web.core');
var local_storage = require('web.local_storage');
var mixins = require('web.mixins');
var utils = require('web_tour.utils');
var RainbowMan = require('web.RainbowMan');
var RunningTourActionHelper = require('web_tour.RunningTourActionHelper');
var ServicesMixin = require('web.ServicesMixin');
var session = require('web.session');
var Tip = require('web_tour.Tip');

var _t = core._t;

var RUNNING_TOUR_TIMEOUT = 10000;

var get_step_key = utils.get_step_key;
var get_running_key = utils.get_running_key;
var get_running_delay_key = utils.get_running_delay_key;
var get_first_visible_element = utils.get_first_visible_element;
var do_before_unload = utils.do_before_unload;

return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
    init: function(parent, consumed_tours) {
        mixins.EventDispatcherMixin.init.call(this);
        this.setParent(parent);

        this.$body = $('body');
        this.active_tooltips = {};
        this.tours = {};
        this.consumed_tours = consumed_tours || [];
        this.running_tour = local_storage.getItem(get_running_key());
        this.running_step_delay = parseInt(local_storage.getItem(get_running_delay_key()), 10) || 10;
        this.edition = (_.last(session.server_version_info) === 'e') ? 'enterprise' : 'community';
        this._log = [];
        console.log('Tour Manager is ready.  running_tour=' + this.running_tour);
    },
    /**
     * Registers a tour described by the following arguments *in order*
     *
     * @param {string} name - tour's name
     * @param {Object} [options] - options (optional), available options are:
     * @param {boolean} [options.test=false] - true if this is only for tests
     * @param {boolean} [options.skip_enabled=false]
     *        true to add a link in its tips to consume the whole tour
     * @param {string} [options.url]
     *        the url to load when manually running the tour
     * @param {boolean} [options.rainbowMan=true]
     *        whether or not the rainbowman must be shown at the end of the tour
     * @param {Deferred} [options.wait_for]
     *        indicates when the tour can be started
     * @param {Object[]} steps - steps' descriptions, each step being an object
     *                     containing a tip description
     */
    register: function() {
        var args = Array.prototype.slice.call(arguments);
        var last_arg = args[args.length - 1];
        var name = args[0];
        if (this.tours[name]) {
            console.warn(_.str.sprintf("Tour %s is already defined", name));
            return;
        }
        var options = args.length === 2 ? {} : args[1];
        var steps = last_arg instanceof Array ? last_arg : [last_arg];
        var tour = {
            name: name,
            steps: steps,
            url: options.url,
            rainbowMan: options.rainbowMan === undefined ? true : !!options.rainbowMan,
            test: options.test,
            wait_for: options.wait_for || $.when(),
        };
        if (options.skip_enabled) {
            tour.skip_link = '<p><span class="o_skip_tour">' + _t('Skip tour') + '</span></p>';
            tour.skip_handler = function (tip) {
                this._deactivate_tip(tip);
                this._consume_tour(name);
            };
        }
        this.tours[name] = tour;
    },
    _register_all: function (do_update) {
        if (this._all_registered) return;
        this._all_registered = true;

        _.each(this.tours, this._register.bind(this, do_update));
    },
    _register: function (do_update, tour, name) {
        if (tour.ready) return $.when();

        var tour_is_consumed = _.contains(this.consumed_tours, name);

        return tour.wait_for.then((function () {
            tour.current_step = parseInt(local_storage.getItem(get_step_key(name))) || 0;
            tour.steps = _.filter(tour.steps, (function (step) {
                return !step.edition || step.edition === this.edition;
            }).bind(this));

            if (tour_is_consumed || tour.current_step >= tour.steps.length) {
                local_storage.removeItem(get_step_key(name));
                tour.current_step = 0;
            }

            tour.ready = true;

            if (do_update && (this.running_tour === name || (!this.running_tour && !tour.test && !tour_is_consumed))) {
                this._to_next_step(name, 0);
                this.update(name);
            }
        }).bind(this));
    },
    run: function (tour_name, step_delay) {
        console.log(_.str.sprintf("Preparing tour %s", tour_name));
        if (this.running_tour) {
            this._deactivate_tip(this.active_tooltips[this.running_tour]);
            this._consume_tour(this.running_tour, _.str.sprintf("Killing tour %s", this.running_tour));
            return;
        }
        var tour = this.tours[tour_name];
        if (!tour) {
            console.warn(_.str.sprintf("Unknown Tour %s", name));
            return;
        }
        console.log(_.str.sprintf("Running tour %s", tour_name));
        this.running_tour = tour_name;
        this.running_step_delay = step_delay || this.running_step_delay;
        local_storage.setItem(get_running_key(), this.running_tour);
        local_storage.setItem(get_running_delay_key(), this.running_step_delay);

        this._deactivate_tip(this.active_tooltips[tour_name]);

        tour.current_step = 0;
        this._to_next_step(tour_name, 0);
        local_storage.setItem(get_step_key(tour_name), tour.current_step);

        if (tour.url) {
            this.pause();
            do_before_unload(null, (function () {
                this.play();
                this.update();
            }).bind(this));

            var url = session.debug ? $.param.querystring(tour.url, {debug: session.debug}) : tour.url;
            window.location.href = window.location.origin + url;
        } else {
            this.update();
        }
    },
    pause: function () {
        this.paused = true;
    },
    play: function () {
        this.paused = false;
    },
    /**
     * Checks for tooltips to activate (only from the running tour or specified tour if there
     * is one, from all active tours otherwise). Should be called each time the DOM changes.
     */
    update: function (tour_name) {
        if (this.paused) return;

        this.$modal_displayed = $('.modal:visible').last();

        tour_name = this.running_tour || tour_name;
        if (tour_name) {
            var tour = this.tours[tour_name];
            if (!tour || !tour.ready) return;

            if (this.running_tour && this.running_tour_timeout === undefined) {
                this._set_running_tour_timeout(this.running_tour, this.active_tooltips[this.running_tour]);
            }
            this._check_for_tooltip(this.active_tooltips[tour_name], tour_name);
        } else {
            _.each(this.active_tooltips, this._check_for_tooltip.bind(this));
        }
    },
    _check_for_tooltip: function (tip, tour_name) {

        if ($('.blockUI').length) {
            this._deactivate_tip(tip);
            this._log.push("blockUI is preventing the tip to be consumed");
            return;
        }

        var $trigger;
        if (tip.in_modal !== false && this.$modal_displayed.length) {
            $trigger = this.$modal_displayed.find(tip.trigger);
        } else {
            $trigger = $(tip.trigger);
        }
        var $visible_trigger = get_first_visible_element($trigger);

        var extra_trigger = true;
        var $extra_trigger = undefined;
        if (tip.extra_trigger) {
            $extra_trigger = $(tip.extra_trigger);
            extra_trigger = get_first_visible_element($extra_trigger).length;
        }

        var triggered = $visible_trigger.length && extra_trigger;
        if (triggered) {
            if (!tip.widget) {
                this._activate_tip(tip, tour_name, $visible_trigger);
            } else {
                tip.widget.update($visible_trigger);
            }
        } else {
            this._deactivate_tip(tip);

            if (this.running_tour === tour_name) {
                this._log.push("_check_for_tooltip");
                this._log.push("- modal_displayed: " + this.$modal_displayed.length);
                this._log.push("- trigger '" + tip.trigger + "': " + $trigger.length);
                this._log.push("- visible trigger '" + tip.trigger + "': " + $visible_trigger.length);
                if ($extra_trigger !== undefined) {
                    this._log.push("- extra_trigger '" + tip.extra_trigger + "': " + $extra_trigger.length);
                    this._log.push("- visible extra_trigger '" + tip.extra_trigger + "': " + extra_trigger);
                }
            }
        }
    },
    _activate_tip: function(tip, tour_name, $anchor) {
        var tour = this.tours[tour_name];
        var tip_info = tip;
        if (tour.skip_link) {
            tip_info = _.extend(_.omit(tip_info, 'content'), {
                content: tip.content + tour.skip_link,
                event_handlers: [{
                    event: 'click',
                    selector: '.o_skip_tour',
                    handler: tour.skip_handler.bind(this, tip),
                }],
            });
        }
        tip.widget = new Tip(this, tip_info);
        if (this.running_tour !== tour_name) {
            tip.widget.on('tip_consumed', this, this._consume_tip.bind(this, tip, tour_name));
        }
        tip.widget.attach_to($anchor).then(this._to_next_running_step.bind(this, tip, tour_name));
    },
    _deactivate_tip: function(tip) {
        if (tip && tip.widget) {
            tip.widget.destroy();
            delete tip.widget;
        }
    },
    _consume_tip: function(tip, tour_name) {
        this._deactivate_tip(tip);
        this._to_next_step(tour_name);

        var is_running = (this.running_tour === tour_name);
        if (is_running) {
            console.log(_.str.sprintf("Tour %s: step %s succeeded", tour_name, tip.trigger));
        }

        if (this.active_tooltips[tour_name]) {
            local_storage.setItem(get_step_key(tour_name), this.tours[tour_name].current_step);
            if (is_running) {
                this._log = [];
                this._set_running_tour_timeout(tour_name, this.active_tooltips[tour_name]);
            }
            this.update(tour_name);
        } else {
            this._consume_tour(tour_name);
        }
    },
    _to_next_step: function (tour_name, inc) {
        var tour = this.tours[tour_name];
        tour.current_step += (inc !== undefined ? inc : 1);
        if (this.running_tour !== tour_name) {
            var index = _.findIndex(tour.steps.slice(tour.current_step), function (tip) {
                return !tip.auto;
            });
            if (index >= 0) {
                tour.current_step += index;
            } else {
                tour.current_step = tour.steps.length;
            }
        }
        this.active_tooltips[tour_name] = tour.steps[tour.current_step];
    },
    _consume_tour: function (tour_name, error) {
        delete this.active_tooltips[tour_name];
        //display rainbow at the end of any tour
        if (this.tours[tour_name].rainbowMan && this.running_tour !== tour_name &&
            this.tours[tour_name].current_step === this.tours[tour_name].steps.length) {
            var $rainbow_message = $('<strong>' +
                                '<b>Good job!</b>' +
                                ' You went through all steps of this tour.' +
                                '</strong>');
            new RainbowMan({message: $rainbow_message}).appendTo(this.$body);
        }
        this.tours[tour_name].current_step = 0;
        local_storage.removeItem(get_step_key(tour_name));
        if (this.running_tour === tour_name) {
            this._stop_running_tour_timeout();
            local_storage.removeItem(get_running_key());
            local_storage.removeItem(get_running_delay_key());
            this.running_tour = undefined;
            this.running_step_delay = undefined;
            if (error) {
                _.each(this._log, function (log) {
                    console.log(log);
                });
                console.log(document.body.outerHTML);
                console.log("error " + error); // phantomJS wait for message starting by error
            } else {
                console.log(_.str.sprintf("Tour %s succeeded", tour_name));
                console.log("ok"); // phantomJS wait for exact message "ok"
            }
            this._log = [];
        } else {
            var self = this;
            this._rpc({
                    model: 'web_tour.tour',
                    method: 'consume',
                    args: [[tour_name]],
                })
                .then(function () {
                    self.consumed_tours.push(tour_name);
                });
        }
    },
    _set_running_tour_timeout: function (tour_name, step) {
        this._stop_running_tour_timeout();
        this.running_tour_timeout = setTimeout((function() {
            this._consume_tour(tour_name, _.str.sprintf("Tour %s failed at step %s", tour_name, step.trigger));
        }).bind(this), (step.timeout || RUNNING_TOUR_TIMEOUT) + this.running_step_delay);
    },
    _stop_running_tour_timeout: function () {
        clearTimeout(this.running_tour_timeout);
        this.running_tour_timeout = undefined;
    },
    _to_next_running_step: function (tip, tour_name) {
        if (this.running_tour !== tour_name) return;
        this._stop_running_tour_timeout();

        var action_helper = new RunningTourActionHelper(tip.widget);
        _.delay((function () {
            do_before_unload(this._consume_tip.bind(this, tip, tour_name));

            if (typeof tip.run === "function") {
                tip.run.call(tip.widget, action_helper);
            } else if (tip.run !== undefined) {
                var m = tip.run.match(/^([a-zA-Z0-9_]+) *(?:\(? *(.+?) *\)?)?$/);
                action_helper[m[1]](m[2]);
            } else {
                action_helper.auto();
            }
        }).bind(this), this.running_step_delay);
    },

    /**
     * Tour predefined steps
     */
    STEPS: {
        MENU_MORE: {
            edition: "community",
            trigger: "body > nav",
            position: "bottom",
            auto: true,
            run: function (actions) {
                actions.auto("#menu_more_container > a");
            },
        },

        TOGGLE_HOME_MENU: {
            edition: "enterprise",
            trigger: ".o_main_navbar .o_menu_toggle",
            content: _t('Click on the <i>Home icon</i> to navigate across apps.'),
            position: "bottom",
        },

        WEBSITE_NEW_PAGE: {
            trigger: "#new-content-menu > a",
            auto: true,
            position: "bottom",
        },
    },
});
});
Beispiel #13
0
odoo.define('web.CrashManager', function (require) {
"use strict";

var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');

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

var map_title ={
    user_error: _lt('Warning'),
    warning: _lt('Warning'),
    access_error: _lt('Access Error'),
    missing_error: _lt('Missing Record'),
    validation_error: _lt('Validation Error'),
    except_orm: _lt('Global Business Error'),
    access_denied: _lt('Access Denied'),
};

var CrashManager = core.Class.extend({
    init: function () {
        var self = this;
        this.active = true;
        this.isConnected = true;

        // listen to unhandled rejected promises, and throw an error when the
        // promise has been rejected due to a crash
        window.addEventListener('unhandledrejection', function (ev) {
            if (ev.reason && ev.reason instanceof Error) {
                var traceback = ev.reason.stack;
                self.show_error({
                    type: _t("Odoo Client Error"),
                    message: '',
                    data: {debug: _t('Traceback:') + "\n" + traceback},
                });
            } else {
                // the rejection is not due to an Error, so prevent the browser
                // from displaying an 'unhandledrejection' error in the console
                ev.stopPropagation();
                ev.stopImmediatePropagation();
                ev.preventDefault();
            }
        });
    },
    enable: function () {
        this.active = true;
    },
    disable: function () {
        this.active = false;
    },
    handleLostConnection: function () {
        var self = this;
        if (!this.isConnected) {
            // already handled, nothing to do.  This can happen when several
            // rpcs are done in parallel and fail because of a lost connection.
            return;
        }
        this.isConnected = false;
        var delay = 2000;
        core.bus.trigger('connection_lost');

        setTimeout(function checkConnection() {
            ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
                core.bus.trigger('connection_restored');
                self.isConnected = true;
            }).guardedCatch(function () {
                // exponential backoff, with some jitter
                delay = (delay * 1.5) + 500*Math.random();
                setTimeout(checkConnection, delay);
            });
        }, delay);
    },
    rpc_error: function(error) {
        if (!this.active) {
            return;
        }
        if (this.connection_lost) {
            return;
        }
        if (error.code === -32098) {
            this.handleLostConnection();
            return;
        }
        var handler = core.crash_registry.get(error.data.name, true);
        if (handler) {
            new (handler)(this, error).display();
            return;
        }
        if (_.has(map_title, error.data.exception_type)) {
            if(error.data.exception_type === 'except_orm'){
                if(error.data.arguments[1]) {
                    error = _.extend({}, error,
                                {
                                    data: _.extend({}, error.data,
                                        {
                                            message: error.data.arguments[1],
                                            title: error.data.arguments[0] !== 'Warning' ? (" - " + error.data.arguments[0]) : '',
                                        })
                                });
                }
                else {
                    error = _.extend({}, error,
                                {
                                    data: _.extend({}, error.data,
                                        {
                                            message: error.data.arguments[0],
                                            title:  '',
                                        })
                                });
                }
            }
            else {
                error = _.extend({}, error,
                            {
                                data: _.extend({}, error.data,
                                    {
                                        message: error.data.arguments[0],
                                        title: map_title[error.data.exception_type] !== 'Warning' ? (" - " + map_title[error.data.exception_type]) : '',
                                    })
                            });
            }

            this.show_warning(error);
        //InternalError

        } else {
            this.show_error(error);
        }
    },
    show_warning: function(error) {
        if (!this.active) {
            return;
        }
        return new Dialog(this, {
            size: 'medium',
            title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"),
            subtitle: error.data.title,
            $content: $(QWeb.render('CrashManager.warning', {error: error}))
        }).open({shouldFocusButtons:true});
    },
    show_error: function(error) {
        if (!this.active) {
            return;
        }
        var dialog = new Dialog(this, {
            title: _.str.capitalize(error.type || error.message) || _t("Odoo Error"),
            $content: $(QWeb.render('CrashManager.error', {error: error}))
        });

        // When the dialog opens, initialize the copy feature and destroy it when the dialog is closed
        var $clipboardBtn;
        var clipboard;
        dialog.opened(function () {
            // When the full traceback is shown, scroll it to the end (useful for better python error reporting)
            dialog.$(".o_error_detail").on("shown.bs.collapse", function (e) {
                e.target.scrollTop = e.target.scrollHeight;
            });

            $clipboardBtn = dialog.$(".o_clipboard_button");
            $clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"});
            clipboard = new window.ClipboardJS($clipboardBtn[0], {
                text: function () {
                    return (_t("Error") + ":\n" + error.message + "\n\n" + error.data.debug).trim();
                },
                // Container added because of Bootstrap modal that give the focus to another element.
                // We need to give to correct focus to ClipboardJS (see in ClipboardJS doc)
                // https://github.com/zenorocha/clipboard.js/issues/155
                container: dialog.el,
            });
            clipboard.on("success", function (e) {
                _.defer(function () {
                    $clipboardBtn.tooltip("show");
                    _.delay(function () {
                        $clipboardBtn.tooltip("hide");
                    }, 800);
                });
            });
        });
        dialog.on("closed", this, function () {
            $clipboardBtn.tooltip('dispose');
            clipboard.destroy();
        });

        return dialog.open();
    },
    show_message: function(exception) {
        return this.show_error({
            type: _t("Odoo Client Error"),
            message: exception,
            data: {debug: ""}
        });
    },
});

/**
 * An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
*/
var ExceptionHandler = {
    /**
     * @param parent The parent.
     * @param error The error object as returned by the JSON-RPC implementation.
     */
    init: function(parent, error) {},
    /**
     * Called to inform to display the widget, if necessary. A typical way would be to implement
     * this interface in a class extending instance.web.Dialog and simply display the dialog in this
     * method.
     */
    display: function() {},
};


/**
 * Handle redirection warnings, which behave more or less like a regular
 * warning, with an additional redirection button.
 */
var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
    init: function(parent, error) {
        this._super(parent);
        this.error = error;
    },
    display: function() {
        var self = this;
        var error = this.error;
        error.data.message = error.data.arguments[0];

        new Dialog(this, {
            size: 'medium',
            title: _.str.capitalize(error.type) || _t("Odoo Warning"),
            buttons: [
                {text: error.data.arguments[2], classes : "btn-primary", click: function() {
                    $.bbq.pushState({
                        'action': error.data.arguments[1],
                        'cids': $.bbq.getState().cids,
                    }, 2);
                    self.destroy();
                    location.reload();
                }},
                {text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
            ],
            $content: QWeb.render('CrashManager.warning', {error: error}),
        }).open();
    }
});

core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandler);

function session_expired(cm) {
    return {
        display: function () {
            cm.show_warning({type: _t("Odoo Session Expired"), data: {message: _t("Your Odoo session expired. Please refresh the current web page.")}});
        }
    }
}
core.crash_registry.add('odoo.http.SessionExpiredException', session_expired);
core.crash_registry.add('werkzeug.exceptions.Forbidden', session_expired);

core.crash_registry.add('504', function (cm) {
    return {
        display: function () {
            cm.show_warning({
                type: _t("Request timeout"),
                data: {message: _t("The operation was interrupted. This usually means that the current operation is taking too much time.")}});
        }
    }
});

return CrashManager;
});
Beispiel #14
0
odoo.define('web.planner.common', function (require) {
"use strict";

var core = require('web.core');
var Dialog = require('web.Dialog');
var Model = require('web.Model');
var Widget = require('web.Widget');
var utils = require('web.utils');

var QWeb = core.qweb;

var _t = core._t;

var Page = core.Class.extend({
    init: function (dom, page_index) {
        var $dom = $(dom);
        this.dom = dom;
        this.hide_from_menu = $dom.attr('hide-from-menu');
        this.hide_mark_as_done = $dom.attr('hide-mark-as-done');
        this.done = false;
        this.menu_item = null;
        this.title = $dom.find('[data-menutitle]').data('menutitle');

        var page_id = this.title.replace(/\s/g, '') + page_index;
        this.set_page_id(page_id);
    },
    set_page_id: function (id) {
        this.id = id;
        $(this.dom).attr('id', id);
    },
    toggle_done: function () {
        this.done = ! this.done;
    },
    get_category_name: function (category_selector) {
        var $page = $(this.dom);

        return $page.parents(category_selector).attr('menu-category-id');
    },
});

var PlannerDialog = Widget.extend({
    template: "PlannerDialog",
    pages: [],
    menu_items: [],
    currently_shown_page: null,
    currently_active_menu_item: null,
    category_selector: 'div[menu-category-id]',
    events: {
        'click li a[href^="#"]:not([data-toggle="collapse"])': 'change_page',
        'click button.mark_as_done': 'click_on_done',
        'click a.btn-next': 'change_to_next_page',
        'click .o_planner_close_block span': 'close_modal',
    },
    init: function(parent, planner) {
        this._super(parent);
        this.planner = planner;
        this.cookie_name = this.planner.planner_application + '_last_page';
        this.set('progress', 0);
        this.pages = [];
        this.menu_items = [];
    },
    /**
     * Fetch the planner's rendered template
     */
    willStart: function() {
        var self = this;
        var res = this._super.apply(this, arguments).then(function() {
            return (new Model('web.planner')).call('render', [self.planner.view_id[0], self.planner.planner_application]);
        }).then(function(template) {
            self.$res = $(template);
        });
        return res;
    },
    start: function() {
        var self = this;
        return this._super.apply(this, arguments).then(function() {
            self.$el.on('keyup', "textarea", function() {
                if (this.scrollHeight != this.clientHeight) {
                    this.style.height = this.scrollHeight + "px";
                }
            }); 
            self.$res.find('.o_planner_page').andSelf().filter('.o_planner_page').each(function(index, dom_page) {
                var page = new Page(dom_page, index);
                self.pages.push(page);
            });

            var $menu = self.render_menu();  // wil use self.$res
            self.$('.o_planner_menu ul').html($menu);
            self.menu_items = self.$('.o_planner_menu li');

            self.pages.forEach(function(page) {
                page.menu_item = self._find_menu_item_by_page_id(page.id);
            });
            self.$el.find('.o_planner_content_wrapper').append(self.$res);

            // update the planner_data with the new inputs of the view
            var actual_vals = self._get_values();
            self.planner.data = _.defaults(self.planner.data, actual_vals);
            // set the default value
            self._set_values(self.planner.data);
            // show last opened page
            self._show_last_open_page();
            self.prepare_planner_event();
        });
    },
    /**
     * This method should be overridden in other planners to bind their custom events once the
     * view is loaded.
     */
    prepare_planner_event: function() {
        var self = this;
        this.on('change:progress', this, function() {
            self.trigger('planner_progress_changed', self.get('progress'));
        });
        this.on('planner_progress_changed', this, this.update_ui_progress_bar);
        this.set('progress', this.planner.progress); // set progress to trigger initial UI update
    },
    _render_done_page: function (page) {
        var mark_as_done_button = this.$('.mark_as_done')
        var mark_as_done_li = mark_as_done_button.find('i');
        var next_button = this.$('a.btn-next');
        var active_menu = $(page.menu_item).find('span');
        if (page.done) {
            active_menu.addClass('fa-check');
            mark_as_done_button.removeClass('btn-primary');
            mark_as_done_li.removeClass('fa-square-o');
            mark_as_done_button.addClass('btn-default');
            mark_as_done_li.addClass('fa-check-square-o');
            next_button.removeClass('btn-default');
            next_button.addClass('btn-primary');

            // page checked animation
            $(page.dom).addClass('marked');
            setTimeout(function() {
                $(page.dom).removeClass('marked');
            }, 1000);
        } else {
            active_menu.removeClass('fa-check');
            mark_as_done_button.removeClass('btn-default');
            mark_as_done_li.removeClass('fa-check-square-o');
            mark_as_done_button.addClass('btn-primary');
            mark_as_done_li.addClass('fa-square-o');
            next_button.removeClass('btn-primary');
            next_button.addClass('btn-default');
        }
        if (page.hide_mark_as_done) {
            next_button.removeClass('btn-default').addClass('btn-primary');
        }
    },
    _show_last_open_page: function () {
        var last_open_page = utils.get_cookie(this.cookie_name);

        if (! last_open_page) {
            last_open_page = this.planner.data.last_open_page || false;
        }

        if (last_open_page && this._find_page_by_id(last_open_page)) {
            this._display_page(last_open_page);
        } else {
            this._display_page(this.pages[0].id);
        }
    },
    update_ui_progress_bar: function(percent) {
        this.$(".progress-bar").css('width', percent+"%");
        this.$(".o_progress_text").text(percent+"%");
    },
    _create_menu_item: function(page, menu_items, menu_item_page_map) {
        var $page = $(page.dom);
        var $menu_item_element = $page.find('h1[data-menutitle]');
        var menu_title = $menu_item_element.data('menutitle') || $menu_item_element.text();

        menu_items.push(menu_title);
        menu_item_page_map[menu_title] = page.id;
    },
    render_menu: function() {
        var self = this;
        var orphan_pages = [];
        var menu_categories = [];
        var menu_item_page_map = {};

        // pages with no category
        self.pages.forEach(function(page) {
            if (! page.hide_from_menu && ! page.get_category_name(self.category_selector)) {
                self._create_menu_item(page, orphan_pages, menu_item_page_map);
            }
        });

        // pages with a category
        self.$res.filter(self.category_selector).each(function(index, menu_category) {
            var $menu_category = $(menu_category);
            var menu_category_item = {
                name: $menu_category.attr('menu-category-id'),
                classes: $menu_category.attr('menu-classes'),
                menu_items: [],
            };

            self.pages.forEach(function(page) {
                if (! page.hide_from_menu && page.get_category_name(self.category_selector) === menu_category_item.name) {
                    self._create_menu_item(page, menu_category_item.menu_items, menu_item_page_map);
                }
            });

            menu_categories.push(menu_category_item);

            // remove the branding used to separate the pages
            self.$res = self.$res.not($menu_category);
            self.$res = self.$res.add($menu_category.contents());
        });

        var menu = QWeb.render('PlannerMenu', {
            'orphan_pages': orphan_pages,
            'menu_categories': menu_categories,
            'menu_item_page_map': menu_item_page_map
        });

        return menu;
    },
    get_next_page_id: function() {
        var self = this;
        var current_page_found = false;
        var next_page_id = null;
        this.pages.every(function(page) {
            if (current_page_found) {
                next_page_id = page.id;
                return false;
            }

            if (page.id === self.currently_shown_page.id) {
                current_page_found = true;
            }

            return true;
        });

        return next_page_id;
    },
    change_to_next_page: function(ev) {
        var next_page_id = this.get_next_page_id();

        ev.preventDefault();

        if (next_page_id) {
            this._display_page(next_page_id);
        }
    },
    change_page: function(ev) {
        ev.preventDefault();
        var page_id = $(ev.currentTarget).attr('href').replace('#', '');
        this._display_page(page_id);
    },
    _find_page_by_id: function (id) {
        var result = _.find(this.pages, function (page) {
            return page.id === id;
        });

        return result;
    },
    _find_menu_item_by_page_id: function (page_id) {
        var result = _.find(this.menu_items, function (menu_item) {
            var $menu_item = $(menu_item);
            return $($menu_item.find('a')).attr('href') === '#' + page_id;
        });

        return result;
    },
    _display_page: function(page_id) {
        var mark_as_done_button = this.$('button.mark_as_done');
        var next_button = this.$('a.btn-next');
        var page = this._find_page_by_id(page_id);
        if (this.currently_active_menu_item) {
            $(this.currently_active_menu_item).removeClass('active');
        }

        var menu_item = this._find_menu_item_by_page_id(page_id);
        $(menu_item).addClass('active');
        this.currently_active_menu_item = menu_item;

        if (this.currently_shown_page) {
            $(this.currently_shown_page.dom).removeClass('show');
        }

        $(page.dom).addClass('show');
        this.currently_shown_page = page;

        if (! this.get_next_page_id()) {
            next_button.hide();
        } else {
            next_button.show();
        }

        if (page.hide_mark_as_done) {
            mark_as_done_button.hide();
        } else {
            mark_as_done_button.show();
        }

        this._render_done_page(this.currently_shown_page);

        this.planner.data.last_open_page = page_id;
        utils.set_cookie(this.cookie_name, page_id, 8*60*60); // create cookie for 8h
        this.$(".modal-body").scrollTop("0");
        autosize(this.$("textarea"));

        this.$('.o_currently_shown_page').text(this.currently_shown_page.title);
    },
    // planner data functions
    _get_values: function(page){
        // if no page_id, take the complete planner
        var base_elem = page ? $(page.dom) : this.$(".o_planner_page");
        var values = {};
        // get the selector for all the input and mark_button
        // only INPUT (select, textearea, input, checkbox and radio), and BUTTON (.mark_button#) are observed
        var inputs = base_elem.find("textarea[id^='input_element'], input[id^='input_element'], select[id^='input_element'], button[id^='mark_button']");
        _.each(inputs, function(elem){
            var $elem = $(elem);
            var tid = $elem.attr('id');
            if ($elem.prop("tagName") === 'BUTTON'){
                if($elem.hasClass('fa-check-square-o')){
                    values[tid] = 'marked';
                }else{
                    values[tid] = '';
                }
            }
            if ($elem.prop("tagName") === 'INPUT' || $elem.prop("tagName") === 'TEXTAREA'){
                var ttype = $elem.attr('type');
                if (ttype === 'checkbox' || ttype === 'radio'){
                    values[tid] = '';
                    if ($elem.is(':checked')){
                        values[tid] = 'checked';
                    }
                }else{
                    values[tid] = $elem.val();
                }
            }
        });

        this.pages.forEach(function(page) {
            values[page.id] = page.done;
        });
        return values;
    },
    _set_values: function(values){
        var self = this;
        _.each(values, function(val, id){
            var $elem = self.$('[id="'+id+'"]');
            if ($elem.prop("tagName") === 'BUTTON'){
                if(val === 'marked'){
                    $elem.addClass('fa-check-square-o btn-default').removeClass('fa-square-o btn-primary');
                    self.$("li a[href=#"+$elem.data('pageid')+"] span").addClass('fa-check');
                }
            }
            if ($elem.prop("tagName") === 'INPUT' || $elem.prop("tagName") === 'TEXTAREA'){
                var ttype = $elem.attr("type");
                if (ttype  === 'checkbox' || ttype === 'radio'){
                    if (val === 'checked') {
                       $elem.attr('checked', 'checked');
                    }
                }else{
                    $elem.val(val);
                }
            }
        });

        this.pages.forEach(function(page) {
            page.done = values[page.id];
            self._render_done_page(page);
        });
    },
    update_planner: function(){
        // update the planner.data with the inputs
        var vals = this._get_values(this.currently_shown_page);
        this.planner.data = _.extend(this.planner.data, vals);

        // re compute the progress percentage
        var total_pages = 0;
        var done_pages = 0;

        this.pages.forEach(function(page) {
            if (! page.hide_mark_as_done) {
                total_pages++;
            }
            if (page.done) {
                done_pages++;
            }
        });

        var percent = parseInt((done_pages / total_pages) * 100, 10);
        this.set('progress', percent);
        this.planner.progress = percent;
        // save data and progress in database
        this._save_planner_data();
    },
    _save_planner_data: function() {
        return (new Model('web.planner')).call('write', [this.planner.id, {'data': JSON.stringify(this.planner.data), 'progress': this.planner.progress}]);
    },
    click_on_done: function(ev) {
        ev.preventDefault();
        this.currently_shown_page.toggle_done();
        this._render_done_page(this.currently_shown_page);
        this.update_planner();
    },
    close_modal: function(ev) {
        ev.preventDefault();
        this.$el.modal('hide');
        this.$el.detach();
    },
    destroy: function() {
        this.$el.modal('hide');
        return this._super.apply(this, arguments);
    }
});

var PlannerHelpMixin = {

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

        var menu = $(ev.currentTarget).data('menu');
        if (menu === 'about') {
            if (!odoo.db_info) {
                var self = this;
                this.rpc("/web/webclient/version_info", {}).done(function(db_info) {
                    self.on_menu_help_about(db_info);
                });
            } else {
                this.on_menu_help_about(odoo.db_info);
            }
        } else if (menu === 'documentation') {
            window.open('https://www.odoo.com/documentation/user', '_blank');
        } else if (menu === 'planner') {
            if (this.dialog) this.show_dialog();
        } else if (menu === 'support') {
            if (odoo.db_info && odoo.db_info.server_version_info[5] === 'c') {
                window.open('https://www.odoo.com/buy', '_blank');
            } else {
                window.location.href = 'mailto:help@odoo.com';
            }
        }
    },

    on_menu_help_about: function(db_info) {
        var $help = $(QWeb.render("PlannerLauncher.about", {db_info: db_info}));
        $help.find('a.oe_activate_debug_mode').click(function (e) {
            e.preventDefault();
            window.location = $.param.querystring( window.location.href, 'debug');
        });
        new Dialog(this, {
            size: 'medium',
            dialogClass: 'o_act_window',
            title: _t("About"),
            $content: $help
        }).open();
    },
};

return {
    PlannerDialog: PlannerDialog,
    PlannerHelpMixin: PlannerHelpMixin,
};

});
Beispiel #15
0
odoo.define('web_tour.TourManager', function(require) {
"use strict";

var core = require('web.core');
var local_storage = require('web.local_storage');
var mixins = require('web.mixins');
var RainbowMan = require('web.rainbow_man');
var ServicesMixin = require('web.ServicesMixin');
var session = require('web.session');
var Tip = require('web_tour.Tip');

var _t = core._t;

var RUNNING_TOUR_TIMEOUT = 10000;

function get_step_key(name) {
    return 'tour_' + name + '_step';
}
function get_running_key() {
    return 'running_tour';
}
function get_running_delay_key() {
    return get_running_key() + "_delay";
}

function get_first_visible_element($elements) {
    for (var i = 0 ; i < $elements.length ; i++) {
        var $i = $elements.eq(i);
        if ($i.is(':visible:hasVisibility')) {
            return $i;
        }
    }
    return $();
}

function do_before_unload(if_unload_callback, if_not_unload_callback) {
    if_unload_callback = if_unload_callback || function () {};
    if_not_unload_callback = if_not_unload_callback || if_unload_callback;

    var old_before = window.onbeforeunload;
    var reload_timeout;
    window.onbeforeunload = function () {
        clearTimeout(reload_timeout);
        window.onbeforeunload = old_before;
        if_unload_callback();
        if (old_before) return old_before.apply(this, arguments);
    };
    reload_timeout = _.defer(function () {
        window.onbeforeunload = old_before;
        if_not_unload_callback();
    });
}

var RunningTourActionHelper = core.Class.extend({
    init: function (tip_widget) {
        this.tip_widget = tip_widget;
    },
    click: function (element) {
        this._click(this._get_action_values(element));
    },
    text: function (text, element) {
        this._text(this._get_action_values(element), text);
    },
    drag_and_drop: function (to, element) {
        this._drag_and_drop(this._get_action_values(element), to);
    },
    keydown: function (keyCodes, element) {
        this._keydown(this._get_action_values(element), keyCodes.split(/[,\s]+/));
    },
    auto: function (element) {
        var values = this._get_action_values(element);
        if (values.consume_event === "input") {
            this._text(values);
        } else {
            this._click(values);
        }
    },
    _get_action_values: function (element) {
        var $e = $(element);
        var $element = element ? get_first_visible_element($e) : this.tip_widget.$anchor;
        if ($element.length === 0) {
            $element = $e.first();
        }
        var consume_event = element ? Tip.getConsumeEventType($element) : this.tip_widget.consume_event;
        return {
            $element: $element,
            consume_event: consume_event,
        };
    },
    _click: function (values) {
        trigger_mouse_event(values.$element, "mouseover");
        values.$element.trigger("mouseenter");
        trigger_mouse_event(values.$element, "mousedown");
        trigger_mouse_event(values.$element, "mouseup");
        trigger_mouse_event(values.$element, "click");
        trigger_mouse_event(values.$element, "mouseout");
        values.$element.trigger("mouseleave");

        function trigger_mouse_event($element, type) {
            var e = document.createEvent("MouseEvents");
            e.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, $element[0]);
            $element[0].dispatchEvent(e);
        }
    },
    _text: function (values, text) {
        this._click(values);

        text = text || "Test";
        if (values.consume_event === "input") {
            values.$element.trigger("keydown").val(text).trigger("keyup").trigger("input");
        } else if (values.$element.is("select")) {
            var $options = values.$element.children("option");
            $options.prop("selected", false).removeProp("selected");
            var $selectedOption = $options.filter(function () { return $(this).val() === text; });
            if ($selectedOption.length === 0) {
                $selectedOption = $options.filter(function () { return $(this).text() === text; });
            }
            $selectedOption.prop("selected", true);
            this._click(values);
        } else {
            values.$element.text(text);
        }
        values.$element.trigger("change");
    },
    _drag_and_drop: function (values, to) {
        var $to = $(to || document.body);

        var elementCenter = values.$element.offset();
        elementCenter.left += values.$element.outerWidth()/2;
        elementCenter.top += values.$element.outerHeight()/2;

        var toCenter = $to.offset();
        toCenter.left += $to.outerWidth()/2;
        toCenter.top += $to.outerHeight()/2;

        values.$element.trigger($.Event("mousedown", {which: 1, pageX: elementCenter.left, pageY: elementCenter.top}));
        values.$element.trigger($.Event("mousemove", {which: 1, pageX: toCenter.left, pageY: toCenter.top}));
        values.$element.trigger($.Event("mouseup", {which: 1, pageX: toCenter.left, pageY: toCenter.top}));
    },
    _keydown: function (values, keyCodes) {
        while (keyCodes.length) {
            var keyCode = +keyCodes.shift();
            values.$element.trigger({type: "keydown", keyCode: keyCode});
            if ((keyCode > 47 && keyCode < 58) // number keys
                || keyCode === 32 // spacebar
                || (keyCode > 64 && keyCode < 91) // letter keys
                || (keyCode > 95 && keyCode < 112) // numpad keys
                || (keyCode > 185 && keyCode < 193) // ;=,-./` (in order)
                || (keyCode > 218 && keyCode < 223)) {   // [\]' (in order))
                document.execCommand("insertText", 0, String.fromCharCode(keyCode));
            }
            values.$element.trigger({type: "keyup", keyCode: keyCode});
        }
    },
});

return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
    init: function(parent, consumed_tours) {
        mixins.EventDispatcherMixin.init.call(this);
        this.setParent(parent);

        this.$body = $('body');
        this.active_tooltips = {};
        this.tours = {};
        this.consumed_tours = consumed_tours || [];
        this.running_tour = local_storage.getItem(get_running_key());
        this.running_step_delay = parseInt(local_storage.getItem(get_running_delay_key()), 10) || 10;
        this.edition = (_.last(session.server_version_info) === 'e') ? 'enterprise' : 'community';
        this._log = [];
    },
    /**
     * Registers a tour described by the following arguments (in order)
     * @param [String] tour's name
     * @param [Object] dict of options (optional), available options are:
     *   test [Boolean] true if the tour is dedicated to tests (it won't be enabled by default)
     *   skip_enabled [Boolean] true to add a link to consume the whole tour in its tips
     *   url [String] the url to load when manually running the tour
     *   rainbowMan [Bool] use to display the rainbow effect at the end of tour
     * @param [Array] dict of steps, each step being a dict containing a tip description
     */
    register: function() {
        var args = Array.prototype.slice.call(arguments);
        var last_arg = args[args.length - 1];
        var name = args[0];
        if (this.tours[name]) {
            console.warn(_.str.sprintf("Tour %s is already defined", name));
            return;
        }
        var options = args.length === 2 ? {} : args[1];
        var steps = last_arg instanceof Array ? last_arg : [last_arg];
        var tour = {
            name: name,
            steps: steps,
            url: options.url,
            rainbowMan: options.rainbowMan || true,
            test: options.test,
            wait_for: options.wait_for || $.when(),
        };
        if (options.skip_enabled) {
            tour.skip_link = '<p><span class="o_skip_tour">' + _t('Skip tour') + '</span></p>';
            tour.skip_handler = function (tip) {
                this._deactivate_tip(tip);
                this._consume_tour(name);
            };
        }
        this.tours[name] = tour;
    },
    _register_all: function (do_update) {
        if (this._all_registered) return;
        this._all_registered = true;

        _.each(this.tours, this._register.bind(this, do_update));
    },
    _register: function (do_update, tour, name) {
        if (tour.ready) return $.when();

        var tour_is_consumed = _.contains(this.consumed_tours, name);

        return tour.wait_for.then((function () {
            tour.current_step = parseInt(local_storage.getItem(get_step_key(name))) || 0;
            tour.steps = _.filter(tour.steps, (function (step) {
                return !step.edition || step.edition === this.edition;
            }).bind(this));

            if (tour_is_consumed || tour.current_step >= tour.steps.length) {
                local_storage.removeItem(get_step_key(name));
                tour.current_step = 0;
            }

            tour.ready = true;

            if (do_update && (this.running_tour === name || (!this.running_tour && !tour.test && !tour_is_consumed))) {
                this._to_next_step(name, 0);
                this.update(name);
            }
        }).bind(this));
    },
    run: function (tour_name, step_delay) {
        if (this.running_tour) {
            this._deactivate_tip(this.active_tooltips[this.running_tour]);
            this._consume_tour(this.running_tour, _.str.sprintf("Killing tour %s", this.running_tour));
            return;
        }
        var tour = this.tours[tour_name];
        if (!tour) {
            console.warn(_.str.sprintf("Unknown Tour %s", name));
            return;
        }
        console.log(_.str.sprintf("Running tour %s", tour_name));
        this.running_tour = tour_name;
        this.running_step_delay = step_delay || this.running_step_delay;
        local_storage.setItem(get_running_key(), this.running_tour);
        local_storage.setItem(get_running_delay_key(), this.running_step_delay);

        this._deactivate_tip(this.active_tooltips[tour_name]);

        tour.current_step = 0;
        this._to_next_step(tour_name, 0);
        local_storage.setItem(get_step_key(tour_name), tour.current_step);

        if (tour.url) {
            this.pause();
            do_before_unload(null, (function () {
                this.play();
                this.update();
            }).bind(this));

            var url = session.debug ? $.param.querystring(tour.url, {debug: session.debug}) : tour.url;
            window.location.href = window.location.origin + url;
        } else {
            this.update();
        }
    },
    pause: function () {
        this.paused = true;
    },
    play: function () {
        this.paused = false;
    },
    /**
     * Checks for tooltips to activate (only from the running tour or specified tour if there
     * is one, from all active tours otherwise). Should be called each time the DOM changes.
     */
    update: function (tour_name) {
        if (this.paused) return;

        this.$modal_displayed = $('.modal:visible').last();

        tour_name = this.running_tour || tour_name;
        if (tour_name) {
            var tour = this.tours[tour_name];
            if (!tour || !tour.ready) return;

            if (this.running_tour && this.running_tour_timeout === undefined) {
                this._set_running_tour_timeout(this.running_tour, this.active_tooltips[this.running_tour]);
            }
            this._check_for_tooltip(this.active_tooltips[tour_name], tour_name);
        } else {
            _.each(this.active_tooltips, this._check_for_tooltip.bind(this));
        }
    },
    _check_for_tooltip: function (tip, tour_name) {
        var $trigger;
        if (tip.in_modal !== false && this.$modal_displayed.length) {
            $trigger = this.$modal_displayed.find(tip.trigger);
        } else {
            $trigger = $(tip.trigger);
        }
        var $visible_trigger = get_first_visible_element($trigger);

        var extra_trigger = true;
        var $extra_trigger = undefined;
        if (tip.extra_trigger) {
            $extra_trigger = $(tip.extra_trigger);
            extra_trigger = get_first_visible_element($extra_trigger).length;
        }

        var triggered = $visible_trigger.length && extra_trigger;
        if (triggered) {
            if (!tip.widget) {
                this._activate_tip(tip, tour_name, $visible_trigger);
            } else {
                tip.widget.update($visible_trigger);
            }
        } else {
            this._deactivate_tip(tip);

            if (this.running_tour === tour_name) {
                this._log.push("_check_for_tooltip");
                this._log.push("- modal_displayed: " + this.$modal_displayed.length);
                this._log.push("- trigger '" + tip.trigger + "': " + $trigger.length);
                this._log.push("- visible trigger '" + tip.trigger + "': " + $visible_trigger.length);
                if ($extra_trigger !== undefined) {
                    this._log.push("- extra_trigger '" + tip.extra_trigger + "': " + $extra_trigger.length);
                    this._log.push("- visible extra_trigger '" + tip.extra_trigger + "': " + extra_trigger);
                }
            }
        }
    },
    _activate_tip: function(tip, tour_name, $anchor) {
        var tour = this.tours[tour_name];
        var tip_info = tip;
        if (tour.skip_link) {
            tip_info = _.extend(_.omit(tip_info, 'content'), {
                content: tip.content + tour.skip_link,
                event_handlers: [{
                    event: 'click',
                    selector: '.o_skip_tour',
                    handler: tour.skip_handler.bind(this, tip),
                }],
            });
        }
        tip.widget = new Tip(this, tip_info);
        if (this.running_tour !== tour_name) {
            tip.widget.on('tip_consumed', this, this._consume_tip.bind(this, tip, tour_name));
        }
        tip.widget.attach_to($anchor).then(this._to_next_running_step.bind(this, tip, tour_name));
    },
    _deactivate_tip: function(tip) {
        if (tip && tip.widget) {
            tip.widget.destroy();
            delete tip.widget;
        }
    },
    _consume_tip: function(tip, tour_name) {
        this._deactivate_tip(tip);
        this._to_next_step(tour_name);

        var is_running = (this.running_tour === tour_name);
        if (is_running) {
            console.log(_.str.sprintf("Tour %s: step %s succeeded", tour_name, tip.trigger));
        }

        if (this.active_tooltips[tour_name]) {
            local_storage.setItem(get_step_key(tour_name), this.tours[tour_name].current_step);
            if (is_running) {
                this._log = [];
                this._set_running_tour_timeout(tour_name, this.active_tooltips[tour_name]);
            }
            this.update(tour_name);
        } else {
            this._consume_tour(tour_name);
        }
    },
    _to_next_step: function (tour_name, inc) {
        var tour = this.tours[tour_name];
        tour.current_step += (inc !== undefined ? inc : 1);
        if (this.running_tour !== tour_name) {
            var index = _.findIndex(tour.steps.slice(tour.current_step), function (tip) {
                return !tip.auto;
            });
            if (index >= 0) {
                tour.current_step += index;
            } else {
                tour.current_step = tour.steps.length;
            }
        }
        this.active_tooltips[tour_name] = tour.steps[tour.current_step];
    },
    _consume_tour: function (tour_name, error) {
        delete this.active_tooltips[tour_name];
        //display rainbow at the end of any tour
        if (this.rainbowMan && this.tours[tour_name].current_step === this.tours[tour_name].steps.length){
            var $rainbow_message = $('<strong>' +
                                '<b>Good job!</b>' +
                                ' You went through all steps of this tour.' +
                                '</strong>');
            new RainbowMan({message: $rainbow_message, click_close: false}).appendTo(this.$body);
        };
        this.tours[tour_name].current_step = 0;
        local_storage.removeItem(get_step_key(tour_name));
        if (this.running_tour === tour_name) {
            this._stop_running_tour_timeout();
            local_storage.removeItem(get_running_key());
            local_storage.removeItem(get_running_delay_key());
            this.running_tour = undefined;
            this.running_step_delay = undefined;
            if (error) {
                _.each(this._log, function (log) {
                    console.log(log);
                });
                console.log(document.body.outerHTML);
                console.log("error " + error); // phantomJS wait for message starting by error
            } else {
                console.log(_.str.sprintf("Tour %s succeeded", tour_name));
                console.log("ok"); // phantomJS wait for exact message "ok"
            }
            this._log = [];
        } else {
            var self = this;
            this._rpc({
                    model: 'web_tour.tour',
                    method: 'consume',
                    args: [[tour_name]],
                })
                .then(function () {
                    self.consumed_tours.push(tour_name);
                });
        }
    },
    _set_running_tour_timeout: function (tour_name, step) {
        this._stop_running_tour_timeout();
        this.running_tour_timeout = setTimeout((function() {
            this._consume_tour(tour_name, _.str.sprintf("Tour %s failed at step %s", tour_name, step.trigger));
        }).bind(this), (step.timeout || RUNNING_TOUR_TIMEOUT) + this.running_step_delay);
    },
    _stop_running_tour_timeout: function () {
        clearTimeout(this.running_tour_timeout);
        this.running_tour_timeout = undefined;
    },
    _to_next_running_step: function (tip, tour_name) {
        if (this.running_tour !== tour_name) return;
        this._stop_running_tour_timeout();

        var action_helper = new RunningTourActionHelper(tip.widget);
        _.delay((function () {
            do_before_unload(this._consume_tip.bind(this, tip, tour_name));

            if (typeof tip.run === "function") {
                tip.run.call(tip.widget, action_helper);
            } else if (tip.run !== undefined) {
                var m = tip.run.match(/^([a-zA-Z0-9_]+) *(?:\(? *(.+?) *\)?)?$/);
                action_helper[m[1]](m[2]);
            } else {
                action_helper.auto();
            }
        }).bind(this), this.running_step_delay);
    },

    /**
     * Tour predefined steps
     */
    STEPS: {
        MENU_MORE: {
            edition: "community",
            trigger: "body > nav",
            position: "bottom",
            auto: true,
            run: function (actions) {
                actions.auto("#menu_more_container > a");
            },
        },

        TOGGLE_APPSWITCHER: {
            edition: "enterprise",
            trigger: ".o_main_navbar .o_menu_toggle",
            content: _t('Click the <i>Home icon</i> to navigate across apps.'),
            position: "bottom",
        },

        WEBSITE_NEW_PAGE: {
            trigger: "#new-content-menu > a",
            auto: true,
            position: "bottom",
        },
    },
});
});
Beispiel #16
0
odoo.define('web_tour.TourManager', function(require) {
"use strict";

var core = require('web.core');
var local_storage = require('web.local_storage');
var Model = require('web.Model');
var Tip = require('web_tour.Tip');

var _t = core._t;

var RUNNING_TOUR_TIMEOUT = 3000;

function getStepKey(name) {
    return 'tour_' + name + '_step';
}
function getRunningKey() {
    return 'running_tour';
}

return core.Class.extend({
    init: function(consumed_tours) {
        this.$body = $('body');
        this.active_tooltips = {};
        this.tours = {};
        this.consumed_tours = consumed_tours;
        this.running_tour = local_storage.getItem(getRunningKey());
        this.TourModel = new Model('web_tour.tour');
    },
    /**
     * Registers a tour described by the following arguments (in order)
     * @param [String] tour's name
     * @param [Object] dict of options (optional), available options are:
     *   test [Boolean] true if the tour is dedicated to tests (it won't be enabled by default)
     *   skip_enabled [Boolean] true to add a link to consume the whole tour in its tips
     *   url [String] the url to load when manually running the tour
     * @param [Array] dict of steps, each step being a dict containing a tip description
     */
    register: function() {
        var args = Array.prototype.slice.call(arguments);
        var last_arg = args[args.length - 1];
        var name = args[0];
        if (this.tours[name]) {
            console.warn(_.str.sprintf(_t("Tour %s is already defined"), name));
            return;
        }
        var options = args.length === 2 ? {} : args[1];
        var steps = last_arg instanceof Array ? last_arg : [last_arg];
        var tour = {
            name: name,
            current_step: parseInt(local_storage.getItem(getStepKey(name))) || 0,
            steps: steps,
            url: options.url,
            test: options.test,
        };
        if (options.skip_enabled) {
            tour.skip_link = '<p><span class="o_skip_tour">' + _t('Skip tour') + '</span></p>';
            tour.skip_handler = function (tip) {
                this._deactivate_tip(tip);
                this._consume_tour(name);
            };
        }
        this.tours[name] = tour;
        if (name === this.running_tour || (!tour.test && !_.contains(this.consumed_tours, name))) {
            this.active_tooltips[name] = steps[tour.current_step];
        }
    },
    run: function(tour_name) {
        if (this.running_tour) {
            console.warn(_.str.sprintf(_t("Killing tour %s"), tour_name));
            this._deactivate_tip(this.active_tooltips[tour_name]);
            this._consume_tour(tour_name);
            return;
        }
        var tour = this.tours[tour_name];
        if (!tour) {
            console.warn(_.str.sprintf(_t("Unknown Tour %s"), name));
            return;
        }
        console.log(_.str.sprintf(_t("Running tour %s"), tour_name));
        local_storage.setItem(getRunningKey(), tour_name);
        if (tour.url) {
            window.location = tour.url;
        }
        this.running_tour = tour_name;
        this.active_tooltips[tour_name] = tour.steps[0];
        this._set_running_tour_timeout(tour_name, tour.steps[0]);
        this.update();
    },
    /**
     * Checks for tooltips to activate (only from the running tour if there is one, from all
     * active tours otherwise). Should be called each time the DOM changes.
     */
    update: function() {
        this.in_modal = this.$body.hasClass('modal-open');
        if (this.running_tour) {
            this._check_for_tooltip(this.active_tooltips[this.running_tour], this.running_tour);
        } else {
            _.each(this.active_tooltips, this._check_for_tooltip.bind(this));
        }
    },
    _check_for_tooltip: function (tip, tour_name) {
        var $trigger = $((this.in_modal ? '.modal ' : '') + tip.trigger).filter(':visible').first();
        var extra_trigger = tip.extra_trigger ? $(tip.extra_trigger).filter(':visible').length : true;
        var triggered = $trigger.length && extra_trigger;
        if (triggered) {
            if (!tip.widget) {
                this._activate_tip(tip, tour_name, $trigger);
            } else {
                tip.widget.update($trigger);
            }
        } else {
            this._deactivate_tip(tip);
        }
    },
    _activate_tip: function(tip, tour_name, $anchor) {
        var tour = this.tours[tour_name];
        var tip_info = tip;
        if (tour.skip_link) {
            tip_info = _.extend(_.omit(tip_info, 'content'), {
                content: tip.content + tour.skip_link,
                event_handlers: [{
                    event: 'click',
                    selector: '.o_skip_tour',
                    handler: tour.skip_handler.bind(this, tip),
                }],
            });
        }
        tip.widget = new Tip(this, $anchor, tip_info);
        tip.widget.appendTo(document.body);
        tip.widget.on('tip_consumed', this, this._consume_tip.bind(this, tip, tour_name));

        if (this.running_tour === tour_name) {
            clearTimeout(this.running_tour_timeout);
            if (tip.run) {
                this._consume_tip(tip, tour_name);
                tip.run.apply(tip);
            }
        }
    },
    _deactivate_tip: function(tip) {
        if (tip.widget) {
            tip.widget.destroy();
            delete tip.widget;
        }
    },
    _consume_tip: function(tip, tour_name) {
        this._deactivate_tip(tip);
        var tour = this.tours[tour_name];
        if (tour.current_step < tour.steps.length - 1) {
            tour.current_step = tour.current_step + 1;
            this.active_tooltips[tour_name] = tour.steps[tour.current_step];
            local_storage.setItem(getStepKey(tour_name), tour.current_step);
            if (this.running_tour === tour_name) {
                this._set_running_tour_timeout(tour_name, this.active_tooltips[tour_name]);
            }
        } else {
            this._consume_tour(tour_name);
        }
    },
    _consume_tour: function(tour_name) {
        delete this.active_tooltips[tour_name];
        this.tours[tour_name].current_step = 0;
        local_storage.removeItem(getStepKey(tour_name));
        if (this.running_tour === tour_name) {
            local_storage.removeItem(getRunningKey());
            this.running_tour = undefined;
            clearTimeout(this.running_tour_timeout);
        } else {
            this.TourModel.call('consume', [tour_name]);
        }
    },
    _set_running_tour_timeout: function(tour_name, step) {
        if (!step.run) { return; } // don't set a timeout if the current step requires a manual action
        var self = this;
        this.running_tour_timeout = setTimeout(function() {
            console.error(_.str.sprintf(_t("Tour %s failed at step %s"), tour_name, step.trigger));
            self._consume_tour(tour_name);
        }, RUNNING_TOUR_TIMEOUT);
    },
});

});
Beispiel #17
0
odoo.define('web.ActionManager', function (require) {
"use strict";

/**
 * ActionManager
 *
 * The action manager is quite important: it makes sure that actions (the Odoo
 * objects, such as a client action, or a act_window) are properly started,
 * and coordinated.
 */

var Bus = require('web.Bus');
var ControlPanel = require('web.ControlPanel');
var Context = require('web.Context');
var core = require('web.core');
var crash_manager = require('web.crash_manager');
var data = require('web.data');
var data_manager = require('web.data_manager');
var Dialog = require('web.Dialog');
var dom = require('web.dom');
var framework = require('web.framework');
var pyeval = require('web.pyeval');
var session = require('web.session');
var ViewManager = require('web.ViewManager');
var Widget = require('web.Widget');

/**
 * Class representing the actions of the ActionManager
 * Basic implementation for client actions that are functions
 */
var Action = core.Class.extend({
    init: function(action) {
        this.action_descr = action;
        this.title = action.display_name || action.name;
    },
    /**
     * This method should restore this previously loaded action
     * Calls on_reverse_breadcrumb_callback if defined
     * @return {Deferred} resolved when widget is enabled
     */
    restore: function() {
        if (this.on_reverse_breadcrumb_callback) {
            return this.on_reverse_breadcrumb_callback();
        }
    },
    /**
     * There is nothing to detach in the case of a client function
     */
    detach: function() {
    },
    /**
     * Destroyer: there is nothing to destroy in the case of a client function
     */
    destroy: function() {
    },
    /**
     * Sets the on_reverse_breadcrumb_callback to be called when coming back to that action
     * @param {Function} [callback] the callback
     */
    set_on_reverse_breadcrumb: function(callback) {
        this.on_reverse_breadcrumb_callback = callback;
    },
    /**
     * Not implemented for client actions
     */
    setScrollTop: function() {
    },
    /**
     * Stores the DOM fragment of the action
     * @param {jQuery} [$fragment] the DOM fragment
     */
    set_fragment: function($fragment) {
        this.$fragment = $fragment;
    },
    /**
     * Not implemented for client actions
     * @return {int} the number of pixels the webclient is scrolled when leaving the action
     */
    getScrollTop: function() {
        return 0;
    },
    /**
     * @return {Object} the description of the action
     */
    get_action_descr: function() {
        return this.action_descr;
    },
    /**
     * @return {Object} dictionnary that will be interpreted to display the breadcrumbs
     */
    get_breadcrumbs: function() {
        return { title: this.title, action: this };
    },
    /**
     * @return {int} the number of views stacked, i.e. 0 for client functions
     */
    get_nb_views: function() {
        return 0;
    },
    /**
     * @return {jQuery} the DOM fragment of the action
     */
    get_fragment: function() {
        return this.$fragment;
    },
    /**
     * @return {string} the active view, i.e. empty for client actions
     */
    get_active_view: function() {
        return '';
    },
});
/**
 * Specialization of Action for client actions that are Widgets
 */
var WidgetAction = Action.extend({
    /**
     * Initializes the WidgetAction
     * Sets the title of the widget
     */
    init: function(action, widget) {
        this._super(action);

        this.widget = widget;
        if (!this.widget.get('title')) {
            this.widget.set('title', this.title);
        }
        this.widget.on('change:title', this, function(widget) {
            this.title = widget.get('title');
        });
    },
    /**
     * Restores WidgetAction by calling do_show on its widget
     */
    restore: function() {
        var self = this;
        return $.when(this._super()).then(function() {
            return self.widget.do_show();
        });
    },
    /**
     * Detaches the action's widget from the DOM
     * @return the widget's $el
     */
    detach: function() {
        // Hack to remove badly inserted nvd3 tooltips ; should be removed when upgrading nvd3 lib
        $('body > .nvtooltip').remove();

        return dom.detach([{widget: this.widget}]);
    },
    /**
     * Destroys the widget
     */
    destroy: function() {
        this.widget.destroy();
    },
});
/**
 * Specialization of WidgetAction for window actions (i.e. ViewManagers)
 */
var ViewManagerAction = WidgetAction.extend({
    /**
     * Restores a ViewManagerAction
     * Switches to the requested view by calling select_view on the ViewManager
     * @param {int} [view_index] the index of the view to select
     */
    restore: function(view_index) {
        var _super = this._super.bind(this);
        return this.widget.select_view(view_index).then(function() {
            return _super();
        });
    },
    /**
     * Sets the on_reverse_breadcrumb_callback and the scrollTop to apply when
     * coming back to that action
     * @param {Function} [callback] the callback
     * @param {int} [scrollTop] the number of pixels to scroll
     */
    set_on_reverse_breadcrumb: function(callback, scrollTop) {
        this._super(callback);
        this.setScrollTop(scrollTop);
    },
    /**
     * Sets the scroll position of the widgets's active_view
     * @todo: replace this with a generic get/set local state mechanism.
     * @see getScrollTop
     *
     * @override
     * @param {integer} [scrollTop] the number of pixels to scroll
     */
    setScrollTop: function (scrollTop) {
        var activeView = this.widget.active_view;
        var viewController = activeView && activeView.controller;
        if (viewController) {
            viewController.setScrollTop(scrollTop);
        }
    },
    /**
     * Returns the current scrolling offset for the current action.  We have to
     * ask nicely the question to the active view, because the answer depends
     * on the view.
     *
     * @todo: replace this mechanism with a generic getLocalState and
     * getLocalState.  Scrolling behaviour is only a part of what we might want
     * to restore.
     *
     * @override
     * @returns {integer} the number of pixels the webclient is currently
     *  scrolled
     */
    getScrollTop: function () {
        var activeView = this.widget.active_view;
        var viewController = activeView && activeView.controller;
        return viewController ? viewController.getScrollTop() : 0;
    },
    /**
     * @return {Array} array of Objects that will be interpreted to display the breadcrumbs
     */
    get_breadcrumbs: function() {
        var self = this;
        return this.widget.view_stack.map(function (view, index) {
            return {
                title: view.controller && view.controller.get('title') || self.title,
                index: index,
                action: self,
            };
        });
    },
    /**
     * @return {int} the number of views stacked in the ViewManager
     */
    get_nb_views: function() {
        return this.widget.view_stack.length;
    },
    /**
     * @return {string} the active view of the ViewManager
     */
    get_active_view: function() {
        return this.widget.active_view.type;
    }
});

var ActionManager = Widget.extend({
    template: 'ActionManager',
    /**
     * Called each time the action manager is attached into the DOM
     */
    on_attach_callback: function() {
        this.is_in_DOM = true;
        if (this.inner_widget && this.inner_widget.on_attach_callback) {
            this.inner_widget.on_attach_callback();
        }
    },
    /**
     * Called each time the action manager is detached from the DOM
     */
    on_detach_callback: function() {
        this.is_in_DOM = false;
        if (this.inner_widget && this.inner_widget.on_detach_callback) {
            this.inner_widget.on_detach_callback();
        }
    },
    init: function(parent, options) {
        this._super(parent);
        this.action_stack = [];
        this.inner_action = null;
        this.inner_widget = null;
        this.webclient = options && options.webclient;
        this.dialog = null;
        this.dialog_widget = null;
        this.on('history_back', this, this.proxy('history_back'));
    },
    start: function() {
        this._super();

        // Instantiate a unique main ControlPanel used by widgets of actions in this.action_stack
        this.main_control_panel = new ControlPanel(this);
        // Listen to event "on_breadcrumb_click" trigerred on the control panel when
        // clicking on a part of the breadcrumbs. Call select_action for this breadcrumb.
        this.main_control_panel.on("on_breadcrumb_click", this, _.debounce(function(action, index) {
            this.select_action(action, index);
        }, 200, true));

        // Listen to event "DOM_updated" to restore the scroll position
        core.bus.on('DOM_updated', this, function() {
            if (this.inner_action) {
                this.trigger_up('scrollTo', {offset: this.inner_action.getScrollTop() || 0});
            }
        });

        // Insert the main control panel into the DOM
        return this.main_control_panel.insertBefore(this.$el);
    },
    dialog_stop: function (reason) {
        if (this.dialog) {
            this.dialog.destroy(reason);
        }
        this.dialog = null;
    },
    /**
     * Add a new action to the action manager
     *
     * @param {Widget} widget typically, widgets added are openerp.web.ViewManager. The action manager uses the stack of actions to handle the breadcrumbs.
     * @param {Object} action_descr new action description
     * @param {Object} options
     * @param options.on_reverse_breadcrumb will be called when breadcrumb is clicked on
     * @param options.clear_breadcrumbs: boolean, if true, action stack is destroyed
     */
    push_action: function(widget, action_descr, options) {
        var self = this;
        var old_action_stack = this.action_stack;
        var old_action = this.inner_action;
        var old_widget = this.inner_widget;
        var actions_to_destroy;
        options = options || {};

        // Empty action_stack or replace last action if requested
        if (options.clear_breadcrumbs) {
            actions_to_destroy = this.action_stack;
            this.action_stack = [];
        } else if (options.replace_last_action && this.action_stack.length > 0) {
            actions_to_destroy = [this.action_stack.pop()];
        }

        // Instantiate the new action
        var new_action;
        if (widget instanceof ViewManager) {
            new_action = new ViewManagerAction(action_descr, widget);
        } else if (widget instanceof Widget) {
            new_action = new WidgetAction(action_descr, widget);
        } else {
            new_action = new Action(action_descr);
        }

        // Set on_reverse_breadcrumb callback on previous inner_action
        if (this.webclient && old_action) {
            old_action.set_on_reverse_breadcrumb(options.on_reverse_breadcrumb, this.webclient.getScrollTop());
        }

        // Update action_stack (must be done before appendTo to properly
        // compute the breadcrumbs and to perform do_push_state)
        this.action_stack.push(new_action);
        this.inner_action = new_action;
        this.inner_widget = widget;

        if (widget.need_control_panel) {
            // Set the ControlPanel bus on the widget to allow it to communicate its status
            widget.set_cp_bus(this.main_control_panel.get_bus());
        }

        // render the inner_widget in a fragment, and append it to the
        // document only when it's ready
        var new_widget_fragment = document.createDocumentFragment();
        return $.when(this.inner_widget.appendTo(new_widget_fragment)).done(function() {
            // Detach the fragment of the previous action and store it within the action
            if (old_action) {
                old_action.set_fragment(old_action.detach());
            }
            if (!widget.need_control_panel) {
                // Hide the main ControlPanel for widgets that do not use it
                self.main_control_panel.do_hide();
            }

            // most of the time, the self.$el element should already be empty,
            // because we detached the old action just a few line up.  However,
            // it may happen that it is not empty, for example when a view
            // manager was unable to load a view because of a crash.  In any
            // case, this is done as a safety measure to avoid the 'double view'
            // situation that we had when the web client was unable to recover
            // from a crash.
            self.$el.empty();

            dom.append(self.$el, new_widget_fragment, {
                in_DOM: self.is_in_DOM,
                callbacks: [{widget: self.inner_widget}],
            });
            if (actions_to_destroy) {
                self.clear_action_stack(actions_to_destroy);
            }
            self.toggle_fullscreen();
            self.trigger_up('current_action_updated', {action: new_action});
        }).fail(function () {
            // Destroy failed action and restore internal state
            new_action.destroy();
            self.action_stack = old_action_stack;
            self.inner_action = old_action;
            self.inner_widget = old_widget;
        });
    },
    setScrollTop: function(scrollTop) {
        if (this.inner_action) {
            this.inner_action.setScrollTop(scrollTop);
        }
    },
    get_breadcrumbs: function () {
        return _.flatten(_.map(this.action_stack, function (action) {
            return action.get_breadcrumbs();
        }), true);
    },
    get_title: function () {
        if (this.action_stack.length === 1) {
            // horrible hack to display the action title instead of "New" for the actions
            // that use a form view to edit something that do not correspond to a real model
            // for example, point of sale "Your Session" or most settings form,
            var action = this.action_stack[0];
            if (action.get_breadcrumbs().length === 1) {
                return action.title;
            }
        }
        var last_breadcrumb = _.last(this.get_breadcrumbs());
        return last_breadcrumb ? last_breadcrumb.title : "";
    },
    get_action_stack: function () {
        return this.action_stack;
    },
    get_inner_action: function() {
        return this.inner_action;
    },
    get_inner_widget: function() {
        return this.inner_widget;
    },
    history_back: function() {
        if (this.dialog) {
            this.dialog.close();
            return;
        }
        var nbViews = this.inner_action.get_nb_views();
        if (nbViews > 1) {
            // Stay on this action, but select the previous view
            return this.select_action(this.inner_action, nbViews - 2);
        }
        var nbActions = this.action_stack.length;
        if (nbActions > 1) {
            // Select the previous action
            var action = this.action_stack[nbActions - 2];
            nbViews = action.get_nb_views();
            return this.select_action(action, nbViews - 1);
        }
        else if (nbActions === 1 && nbViews === 1) {
            return this.select_action(this.action_stack[0], 0);
        }
        return $.Deferred().reject();
    },
    select_action: function(action, index) {
        var self = this;
        var def = this.webclient && this.webclient.clear_uncommitted_changes() || $.when();

        return def.then(function() {
            // Set the new inner_action/widget and update the action stack
            var old_action = self.inner_action;
            var action_index = self.action_stack.indexOf(action);
            var to_destroy = self.action_stack.splice(action_index + 1);
            self.inner_action = action;
            self.inner_widget = action.widget;

            return $.when(action.restore(index)).done(function() {
                // Hide the ControlPanel if the widget doesn't use it
                if (!self.inner_widget.need_control_panel) {
                    self.main_control_panel.do_hide();
                }
                // Attach the DOM of the action and restore the scroll position only if necessary
                if (action !== old_action) {
                    // Clear the action stack (this also removes the current action from the DOM)
                    self.clear_action_stack(to_destroy);

                    // Append the fragment of the action to restore to self.$el
                    dom.append(self.$el, action.get_fragment(), {
                        in_DOM: self.is_in_DOM,
                        callbacks: [{widget: action.widget}],
                    });
                }
                self.trigger_up('current_action_updated', {action: action});
            });
        }).fail(function() {
            return $.Deferred().reject();
        });
    },
    clear_action_stack: function(action_stack) {
        _.map(action_stack || this.action_stack, function(action) {
            action.destroy();
        });
        if (!action_stack) {
            this.action_stack = [];
            this.inner_action = null;
            this.inner_widget = null;
        }
        this.toggle_fullscreen();
    },
    toggle_fullscreen: function() {
        var is_fullscreen = _.some(this.action_stack, function(action) {
            return action.action_descr.target === "fullscreen";
        });
        this.trigger_up("toggle_fullscreen", {fullscreen: is_fullscreen});
    },
    do_push_state: function(state) {
        if (!this.webclient || this.dialog) {
            return;
        }
        state = state || {};
        if (this.inner_action) {
            var inner_action_descr = this.inner_action.get_action_descr();
            if (inner_action_descr._push_me === false) {
                // this action has been explicitly marked as not pushable
                return;
            }
            state.title = this.get_title();
            if(inner_action_descr.type == 'ir.actions.act_window') {
                state.model = inner_action_descr.res_model;
            }
            if (inner_action_descr.menu_id) {
                state.menu_id = inner_action_descr.menu_id;
            }
            if (inner_action_descr.id) {
                state.action = inner_action_descr.id;
            } else if (inner_action_descr.type == 'ir.actions.client') {
                state.action = inner_action_descr.tag;
                var params = {};
                _.each(inner_action_descr.params, function(v, k) {
                    if(_.isString(v) || _.isNumber(v)) {
                        params[k] = v;
                    }
                });
                state = _.extend(params || {}, state);
            }
            if (inner_action_descr.context) {
                var active_id = inner_action_descr.context.active_id;
                if (active_id) {
                    state.active_id = active_id;
                }
                var active_ids = inner_action_descr.context.active_ids;
                if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
                    // We don't push active_ids if it's a single element array containing the active_id
                    // This makes the url shorter in most cases.
                    state.active_ids = inner_action_descr.context.active_ids.join(',');
                }
            }
        }
        this.webclient.do_push_state(state);
    },
    do_load_state: function(state, warm) {
        var self = this;
        var action_loaded;
        if (state.action) {
            if (_.isString(state.action) && core.action_registry.contains(state.action)) {
                var action_client = {
                    type: "ir.actions.client",
                    tag: state.action,
                    params: state,
                    _push_me: state._push_me,
                };
                if (warm) {
                    this.null_action();
                }
                action_loaded = this.do_action(action_client);
            } else {
                var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action;
                if (run_action) {
                    var add_context = {};
                    if (state.active_id) {
                        add_context.active_id = state.active_id;
                    }
                    if (state.active_ids) {
                        // The jQuery BBQ plugin does some parsing on values that are valid integers.
                        // It means that if there's only one item, it will do parseInt() on it,
                        // otherwise it will keep the comma seperated list as string.
                        add_context.active_ids = state.active_ids.toString().split(',').map(function(id) {
                            return parseInt(id, 10) || id;
                        });
                    } else if (state.active_id) {
                        add_context.active_ids = [state.active_id];
                    }
                    add_context.params = state;
                    if (warm) {
                        this.null_action();
                    }
                    action_loaded = this.do_action(state.action, {
                        additional_context: add_context,
                        res_id: state.id,
                        view_type: state.view_type,
                    });
                }
            }
        } else if (state.model && state.id) {
            // TODO handle context & domain ?
            if (warm) {
                this.null_action();
            }
            var action = {
                res_model: state.model,
                res_id: state.id,
                type: 'ir.actions.act_window',
                views: [[_.isNumber(state.view_id) ? state.view_id : false, 'form']]
            };
            action_loaded = this.do_action(action);
        } else if (state.sa) {
            // load session action
            if (warm) {
                this.null_action();
            }
            action_loaded = this._rpc({
                    route: '/web/session/get_session_action',
                    params: {key: state.sa},
                })
                .then(function(action) {
                    if (action) {
                        return self.do_action(action);
                    }
                });
        }

        return $.when(action_loaded || null).then(function() {
            if (self.inner_widget && self.inner_widget.do_load_state) {
                return self.inner_widget.do_load_state(state, warm);
            }
        });
    },
    /**
     * Execute an OpenERP action
     *
     * @param {Number|String|String|Object} action Can be either an action id, an action XML id, a client action tag or an action descriptor.
     * @param {Object} [options]
     * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
     * @param {Boolean} [options.replace_breadcrumb=false] Replace the current breadcrumb with the action
     * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
     * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
     * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
     * @param {Function} [options.action_menu_id] Manually set the menu id on the fly.
     * @param {Object} [options.additional_context] Additional context to be merged with the action's context.
     * @return {jQuery.Deferred} Action loaded
     */
    do_action: function(action, options) {
        options = _.defaults(options || {}, {
            clear_breadcrumbs: false,
            replace_last_action: false,
            on_reverse_breadcrumb: function() {},
            hide_breadcrumb: false,
            on_close: function() {},
            on_load: function() {},
            action_menu_id: null,
            additional_context: {},
        });
        if (action === false) {
            action = { type: 'ir.actions.act_window_close' };
        } else if (_.isString(action) && core.action_registry.contains(action)) {
            var action_client = { type: "ir.actions.client", tag: action, params: {} };
            return this.do_action(action_client, options);
        } else if (_.isNumber(action) || _.isString(action)) {
            var self = this;
            var additional_context = {
                active_id : options.additional_context.active_id,
                active_ids : options.additional_context.active_ids,
                active_model : options.additional_context.active_model
            };
            return data_manager.load_action(action, additional_context).then(function(result) {
                return self.do_action(result, options);
            });
        }

        core.bus.trigger('action', action);

        // Force clear breadcrumbs if action's target is main
        options.clear_breadcrumbs = (action.target === 'main') || options.clear_breadcrumbs;

        // Ensure context & domain are evaluated and can be manipulated/used
        var user_context = this.getSession().user_context;
        var ncontext = new Context(user_context, options.additional_context, action.context || {});
        action.context = pyeval.eval('context', ncontext);
        if (action.context.active_id || action.context.active_ids) {
            // Here we assume that when an `active_id` or `active_ids` is used
            // in the context, we are in a `related` action, so we disable the
            // searchview's default custom filters.
            action.context.search_disable_custom_filters = true;
        }
        if (action.domain) {
            action.domain = pyeval.eval(
                'domain', action.domain, action.context || {});
        }

        if (!action.type) {
            console.error("No type for action", action);
            return $.Deferred().reject();
        }

        var type = action.type.replace(/\./g,'_');
        action.menu_id = options.action_menu_id;
        action.res_id = options.res_id || action.res_id;
        action.context.params = _.extend({ 'action' : action.id }, action.context.params);
        if (!(type in this)) {
            console.error("Action manager can't handle action of type " + action.type, action);
            return $.Deferred().reject();
        }

        // Special case for Dashboards, this should definitively be done upstream
        if (action.res_model === 'board.board' && action.view_mode === 'form') {
            action.target = 'inline';
            _.extend(action.flags, {
                headless: true,
                views_switcher: false,
                display_title: false,
                search_view: false,
                pager: false,
                sidebar: false,
                action_buttons: false
            });
        } else {
            var popup = action.target === 'new';
            var inline = action.target === 'inline' || action.target === 'inlineview';
            var form = _.str.startsWith(action.view_mode, 'form');
            action.flags = _.defaults(action.flags || {}, {
                views_switcher : !popup && !inline,
                search_view : !(popup && form) && !inline,
                action_buttons : !popup && !inline,
                sidebar : !popup && !inline,
                pager : (!popup || !form) && !inline,
                display_title : !popup,
                headless: (popup || inline) && form,
                search_disable_custom_filters: action.context && action.context.search_disable_custom_filters,
            });
        }

        return $.when(this[type](action, options)).then(function (executor_action) {
            options.on_load(executor_action);
            return action;
        });
    },
    null_action: function() {
        this.dialog_stop();
        this.clear_action_stack();
    },
    /**
     *
     * @param {Object} executor
     * @param {Object} executor.action original action
     * @param {Function<instance.web.Widget>} executor.widget function used to fetch the widget instance
     * @param {String} executor.klass CSS class to add on the dialog root, if action.target=new
     * @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget
     * @param {Object} options
     * @return {Deferred<*>}
     */
    ir_actions_common: function(executor, options) {
        var self = this;
        if (executor.action.target === 'new') {
            var pre_dialog = (this.dialog && !this.dialog.isDestroyed()) ? this.dialog : null;
            if (pre_dialog){
                // prevent previous dialog to consider itself closed,
                // right now, as we're opening a new one (prevents
                // reload of original form view)
                pre_dialog.off('closed', null, pre_dialog.on_close);
            }
            if (this.dialog_widget && !this.dialog_widget.isDestroyed()) {
                this.dialog_widget.destroy();
            }
            // explicitly passing a closing action to dialog_stop() prevents
            // it from reloading the original form view
            this.dialog_stop(executor.action);
            this.dialog = new Dialog(this, _.defaults(options || {}, {
                title: executor.action.name,
                dialogClass: executor.klass,
                buttons: [],
                size: executor.action.context.dialog_size,
            }));

            // chain on_close triggers with previous dialog, if any
            this.dialog.on_close = function(){
                options.on_close.apply(null, arguments);
                if (pre_dialog && pre_dialog.on_close){
                    // no parameter passed to on_close as this will
                    // only be called when the last dialog is truly
                    // closing, and *should* trigger a reload of the
                    // underlying form view (see comments above)
                    pre_dialog.on_close();
                }
                if (!pre_dialog) {
                    self.dialog = null;
                }
            };
            this.dialog.on("closed", null, this.dialog.on_close);
            this.dialog_widget = executor.widget();
            var $dialogFooter;
            if (this.dialog_widget instanceof ViewManager) {
                executor.action.viewManager = this.dialog_widget;
                $dialogFooter = $('<div/>'); // fake dialog footer in which view
                                             // manager buttons will be put
                _.defaults(this.dialog_widget.flags, {
                    $buttons: $dialogFooter,
                    footer_to_buttons: true,
                });
                if (this.dialog_widget.action.view_mode === 'form') {
                    this.dialog_widget.flags.headless = true;
                }
            }
            if (this.dialog_widget.need_control_panel) {
                // Set a fake bus to Dialogs needing a ControlPanel as they should not
                // communicate with the main ControlPanel
                this.dialog_widget.set_cp_bus(new Bus());
            }
            this.dialog_widget.setParent(this.dialog);

            var fragment = document.createDocumentFragment();
            return this.dialog_widget.appendTo(fragment).then(function () {
                var def = $.Deferred();
                self.dialog.opened().then(function () {
                    dom.append(self.dialog.$el, fragment, {
                        in_DOM: true,
                        callbacks: [{widget: self.dialog_widget}],
                    });
                    if ($dialogFooter) {
                        self.dialog.$footer.empty().append($dialogFooter.contents());
                    }
                    if (options.state && self.dialog_widget.do_load_state) {
                        return self.dialog_widget.do_load_state(options.state);
                    }
                })
                .done(def.resolve.bind(def))
                .fail(def.reject.bind(def));
                self.dialog.open();
                return def;
            }).then(function () {
                return executor.action;
            });
        }
        var def = this.inner_action && this.webclient && this.webclient.clear_uncommitted_changes() || $.when();
        return def.then(function() {
            self.dialog_stop(executor.action);
            return self.push_action(executor.action.viewManager = executor.widget(), executor.action, options).then(function () {
                return executor.action;
            });
        }).fail(function() {
            return $.Deferred().reject();
        });
    },
    ir_actions_act_window: function (action, options) {
        var self = this;

        var flags = action.flags || {};
        if (!('auto_search' in flags)) {
            flags.auto_search = action.auto_search !== false;
        }
        options.action = action;
        options.action_manager = this;
        var dataset = new data.DataSetSearch(this, action.res_model, action.context, action.domain);
        if (action.res_id) {
            dataset.ids.push(action.res_id);
            dataset.index = 0;
        }
        var views = action.views;

        return this.ir_actions_common({
            widget: function () {
                return new ViewManager(self, dataset, views, flags, options);
            },
            action: action,
            klass: 'o_act_window',
        }, options);
    },
    ir_actions_client: function (action, options) {
        var self = this;
        var ClientWidget = core.action_registry.get(action.tag);
        if (!ClientWidget) {
            return self.do_warn("Action Error", "Could not find client action '" + action.tag + "'.");
        }
        if (!(ClientWidget.prototype instanceof Widget)) {
            var next;
            if ((next = ClientWidget(this, action))) {
                return this.do_action(next, options);
            }
            return $.when();
        }

        return this.ir_actions_common({
            widget: function () {
                return new ClientWidget(self, action, options);
            },
            action: action,
            klass: 'oe_act_client',
        }, options).then(function () {
            if (action.tag !== 'reload') {self.do_push_state({});}
        });
    },
    ir_actions_act_window_close: function (action, options) {
        if (!this.dialog) {
            options.on_close();
        }
        this.dialog_stop();
        // Display rainbowman on appropriate actions
        if (action.effect) {
            this.trigger_up('show_effect', action.effect);
        }

        return $.when();
    },
    ir_actions_server: function (action, options) {
        var self = this;
        return this._rpc({
                route: '/web/action/run',
                params: {
                    action_id: action.id,
                    context: action.context || {}
                },
            })
            .then(function (action) {
                return self.do_action(action, options);
            });
    },
    ir_actions_report: function(action, options) {
        var self = this;
        framework.blockUI();
        action = _.clone(action);
        var eval_contexts = ([session.user_context] || []).concat([action.context]);
        action.context = pyeval.eval('contexts',eval_contexts);

        // iOS devices doesn't allow iframe use the way we do it,
        // opening a new window seems the best way to workaround
        if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
            var params = {
                action: JSON.stringify(action),
                token: new Date().getTime()
            };
            var url = session.url('/web/report', params);
            framework.unblockUI();
            $('<a href="'+url+'" target="_blank"></a>')[0].click();
            return;
        }
        var c = crash_manager;
        return $.Deferred(function (d) {
            session.get_file({
                url: '/web/report',
                data: {action: JSON.stringify(action)},
                complete: framework.unblockUI,
                success: function(){
                    if (!self.dialog) {
                        options.on_close();
                    }
                    self.dialog_stop();
                    d.resolve();
                },
                error: function () {
                    c.rpc_error.apply(c, arguments);
                    d.reject();
                }
            });
        });
    },
    ir_actions_act_url: function (action, options) {
        var url = action.url;
        if (session.debug && url && url.length && url[0] === '/') {
            url = $.param.querystring(url, {debug: session.debug});
        }

        if (action.target === 'self') {
            framework.redirect(url);
            return $.Deferred(); // The action is finished only when the redirection is done
        } else {
            window.open(url, '_blank');
            options.on_close();
        }
        return $.when();
    },
});

return ActionManager;

});
Beispiel #18
0
odoo.define('web.Session', function (require) {
"use strict";

var ajax = require('web.ajax');
var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var mixins = require('web.mixins');
var utils = require('web.utils');

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

// To do: refactor session. Session accomplishes several concerns (rpc,
// configuration, currencies (wtf?), user permissions...). They should be
// clarified and separated.

var Session = core.Class.extend(mixins.EventDispatcherMixin, {
    /**

    @param parent The parent of the newly created object.
    or `null` if the server to contact is the origin server.
    @param {Dict} options A dictionary that can contain the following options:

        * "override_session": Default to false. If true, the current session object will
          not try to re-use a previously created session id stored in a cookie.
        * "session_id": Default to null. If specified, the specified session_id will be used
          by this session object. Specifying this option automatically implies that the option
          "override_session" is set to true.
     */
    init: function (parent, origin, options) {
        mixins.EventDispatcherMixin.init.call(this, parent);
        options = options || {};
        this.module_list = (options.modules && options.modules.slice()) || (window.odoo._modules && window.odoo._modules.slice()) || [];
        this.server = null;
        this.session_id = options.session_id || null;
        this.override_session = options.override_session || !!options.session_id || false;
        this.avoid_recursion = false;
        this.use_cors = options.use_cors || false;
        this.setup(origin);
        this.debug = config.debug;

        // for historic reasons, the session requires a name to properly work
        // (see the methods get_cookie and set_cookie).  We should perhaps
        // remove it totally (but need to make sure the cookies are properly set)
        this.name = "instance0";
        // TODO: session store in cookie should be optional
        this.qweb_mutex = new concurrency.Mutex();
        this.currencies = {};
        this._groups_def = {};
    },
    setup: function (origin, options) {
        // must be able to customize server
        var window_origin = location.protocol + "//" + location.host;
        origin = origin ? origin.replace( /\/+$/, '') : window_origin;
        if (!_.isUndefined(this.origin) && this.origin !== origin)
            throw new Error('Session already bound to ' + this.origin);
        else
            this.origin = origin;
        this.prefix = this.origin;
        this.server = this.origin; // keep chs happy
        this.origin_server = this.origin === window_origin;
        options = options || {};
        if ('use_cors' in options) {
            this.use_cors = options.use_cors;
        }
    },
    /**
     * Setup a session
     */
    session_bind: function (origin) {
        var self = this;
        this.setup(origin);
        qweb.default_dict._s = this.origin;
        this.uid = null;
        this.username = null;
        this.user_context= {};
        this.db = null;
        this.module_loaded = {};
        _(this.module_list).each(function (mod) {
            self.module_loaded[mod] = true;
        });
        this.active_id = null;
        return this.session_init();
    },
    /**
     * Init a session, reloads from cookie, if it exists
     */
    session_init: function () {
        var self = this;
        var def = this.session_reload();

        if (this.is_frontend) {
            return def.then(function () {
                return self.load_translations();
            });
        }

        return def.then(function () {
            var modules = self.module_list.join(',');
            var deferred = self.load_qweb(modules);
            if (self.session_is_valid()) {
                return deferred.then(function () { return self.load_modules(); });
            }
            return $.when(
                    deferred,
                    self.rpc('/web/webclient/bootstrap_translations', {mods: self.module_list})
                        .then(function (trans) {
                            _t.database.set_bundle(trans);
                        })
            );
        });
    },
    session_is_valid: function () {
        var db = $.deparam.querystring().db;
        if (db && this.db !== db) {
            return false;
        }
        return !!this.uid;
    },
    /**
     * The session is validated by restoration of a previous session
     */
    session_authenticate: function () {
        var self = this;
        return $.when(this._session_authenticate.apply(this, arguments)).then(function () {
            return self.load_modules();
        });
    },
    /**
     * The session is validated either by login or by restoration of a previous session
     */
    _session_authenticate: function (db, login, password) {
        var self = this;
        var params = {db: db, login: login, password: password};
        return this.rpc("/web/session/authenticate", params).then(function (result) {
            if (!result.uid) {
                return $.Deferred().reject();
            }
            delete result.session_id;
            _.extend(self, result);
        });
    },
    session_logout: function () {
        $.bbq.removeState();
        return this.rpc("/web/session/destroy", {});
    },
    user_has_group: function (group) {
        if (!this.uid) {
            return $.when(false);
        }
        var def = this._groups_def[group];
        if (!def) {
            def = this._groups_def[group] = this.rpc('/web/dataset/call_kw/res.users/has_group', {
                "model": "res.users",
                "method": "has_group",
                "args": [group],
                "kwargs": {}
            });
        }
        return def;
    },
    get_cookie: function (name) {
        if (!this.name) { return null; }
        var nameEQ = this.name + '|' + name + '=';
        var cookies = document.cookie.split(';');
        for(var i=0; i<cookies.length; ++i) {
            var cookie = cookies[i].replace(/^\s*/, '');
            if(cookie.indexOf(nameEQ) === 0) {
                try {
                    return JSON.parse(decodeURIComponent(cookie.substring(nameEQ.length)));
                } catch(err) {
                    // wrong cookie, delete it
                    this.set_cookie(name, '', -1);
                }
            }
        }
        return null;
    },
    /**
     * Create a new cookie with the provided name and value
     *
     * @private
     * @param name the cookie's name
     * @param value the cookie's value
     * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete
     */
    set_cookie: function (name, value, ttl) {
        if (!this.name) { return; }
        ttl = ttl || 24*60*60*365;
        utils.set_cookie(this.name + '|' + name, value, ttl);
    },
    /**
     * Load additional web addons of that instance and init them
     *
     */
    load_modules: function () {
        var self = this;
        var modules = odoo._modules;
        var all_modules = _.uniq(self.module_list.concat(modules));
        var to_load = _.difference(modules, self.module_list).join(',');
        this.module_list = all_modules;

        var loaded = $.when(self.load_translations());
        var locale = "/web/webclient/locale/" + self.user_context.lang || 'en_US';
        var file_list = [ locale ];
        if(to_load.length) {
            loaded = $.when(
                loaded,
                self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.load_css.bind(self)),
                self.load_qweb(to_load),
                self.rpc('/web/webclient/jslist', {mods: to_load}).done(function (files) {
                    file_list = file_list.concat(files);
                })
            );
        }
        return loaded.then(function () {
            return self.load_js(file_list);
        }).done(function () {
            self.on_modules_loaded();
            self.trigger('module_loaded');
       });
    },
    load_translations: function () {
        return _t.database.load_translations(this, this.module_list, this.user_context.lang, this.translationURL);
    },
    load_css: function (files) {
        var self = this;
        _.each(files, function (file) {
            ajax.loadCSS(self.url(file, null));
        });
    },
    load_js: function (files) {
        var self = this;
        var d = $.Deferred();
        if (files.length !== 0) {
            var file = files.shift();
            var url = self.url(file, null);
            ajax.loadJS(url).done(d.resolve);
        } else {
            d.resolve();
        }
        return d;
    },
    load_qweb: function (mods) {
        this.qweb_mutex.exec(function () {
            return $.get('/web/webclient/qweb?mods=' + mods).then(function (doc) {
                if (!doc) { return; }
                qweb.add_template(doc);
            });
        });
        return this.qweb_mutex.def;
    },
    on_modules_loaded: function () {
        var openerp = window.openerp;
        for(var j=0; j<this.module_list.length; j++) {
            var mod = this.module_list[j];
            if(this.module_loaded[mod])
                continue;
            openerp[mod] = {};
            // init module mod
            var fct = openerp._openerp[mod];
            if(typeof(fct) === "function") {
                openerp._openerp[mod] = {};
                for (var k in fct) {
                    openerp._openerp[mod][k] = fct[k];
                }
                fct(openerp, openerp._openerp[mod]);
            }
            this.module_loaded[mod] = true;
        }
    },
    get_currency: function (currency_id) {
        return this.currencies[currency_id];
    },
    get_file: function (options) {
        if (this.override_session){
            options.data.session_id = this.session_id;
        }
        options.session = this;
        return ajax.get_file(options);
    },
    /**
     * (re)loads the content of a session: db name, username, user id, session
     * context and status of the support contract
     *
     * @returns {$.Deferred} deferred indicating the session is done reloading
     */
    session_reload: function () {
        var result = _.extend({}, window.odoo.session_info);
        delete result.session_id;
        _.extend(this, result);
        return $.when();
    },
    check_session_id: function () {
        var self = this;
        if (this.avoid_recursion)
            return $.when();
        if (this.session_id)
            return $.when(); // we already have the session id
        if (!this.use_cors && (this.override_session || ! this.origin_server)) {
            // If we don't use the origin server we consider we should always create a new session.
            // Even if some browsers could support cookies when using jsonp that behavior is
            // not consistent and the browser creators are tending to removing that feature.
            this.avoid_recursion = true;
            return this.rpc("/gen_session_id", {}).then(function (result) {
                self.session_id = result;
            }).always(function () {
                self.avoid_recursion = false;
            });
        }
        return $.when();
    },
    /**
     * Executes an RPC call, registering the provided callbacks.
     *
     * Registers a default error callback if none is provided, and handles
     * setting the correct session id and session context in the parameter
     * objects
     *
     * @param {String} url RPC endpoint
     * @param {Object} params call parameters
     * @param {Object} options additional options for rpc call
     * @returns {jQuery.Deferred} jquery-provided ajax deferred
     */
    rpc: function (url, params, options) {
        var self = this;
        options = _.clone(options || {});
        var shadow = options.shadow || false;
        options.headers = _.extend({}, options.headers);
        if (odoo.debug) {
            options.headers["X-Debug-Mode"] = $.deparam($.param.querystring()).debug;
        }

        delete options.shadow;

        return self.check_session_id().then(function () {
            // TODO: remove
            if (! _.isString(url)) {
                _.extend(options, url);
                url = url.url;
            }
            // TODO correct handling of timeouts
            if (! shadow)
                self.trigger('request');
            var fct;
            if (self.origin_server) {
                fct = ajax.jsonRpc;
                if (self.override_session) {
                    options.headers["X-Openerp-Session-Id"] = self.session_id || '';
                }
            } else if (self.use_cors) {
                fct = ajax.jsonRpc;
                url = self.url(url, null);
                options.session_id = self.session_id || '';
                if (self.override_session) {
                    options.headers["X-Openerp-Session-Id"] = self.session_id || '';
                }
            } else {
                fct = ajax.jsonpRpc;
                url = self.url(url, null);
                options.session_id = self.session_id || '';
            }
            var p = fct(url, "call", params, options);
            p = p.then(function (result) {
                if (! shadow)
                    self.trigger('response');
                return result;
            }, function (type, error, textStatus, errorThrown) {
                if (type === "server") {
                    if (! shadow)
                        self.trigger('response');
                    if (error.code === 100) {
                        self.uid = false;
                    }
                    return $.Deferred().reject(error, $.Event());
                } else {
                    if (! shadow)
                        self.trigger('response_failed');
                    var nerror = {
                        code: -32098,
                        message: "XmlHttpRequestError " + errorThrown,
                        data: {type: "xhr"+textStatus, debug: error.responseText, objects: [error, errorThrown] }
                    };
                    return $.Deferred().reject(nerror, $.Event());
                }
            });
            return p.fail(function () { // Allow deferred user to disable rpc_error call in fail
                p.fail(function (error, event) {
                    if (!event.isDefaultPrevented()) {
                        self.trigger('error', error, event);
                    }
                });
            });
        });
    },
    url: function (path, params) {
        params = _.extend(params || {});
        if (this.override_session || (! this.origin_server))
            params.session_id = this.session_id;
        var qs = $.param(params);
        if (qs.length > 0)
            qs = "?" + qs;
        var prefix = _.any(['http://', 'https://', '//'], function (el) {
            return path.length >= el.length && path.slice(0, el.length) === el;
        }) ? '' : this.prefix;
        return prefix + path + qs;
    },
    /**
     * Returns the time zone difference (in minutes) from the current locale
     * (host system settings) to UTC, for a given date. The offset is positive
     * if the local timezone is behind UTC, and negative if it is ahead.
     *
     * @param {string | moment} date a valid string date or moment instance
     * @returns {integer}
     */
    getTZOffset: function (date) {
        return -new Date(date).getTimezoneOffset();
    },
});

return Session;

});
Beispiel #19
0
odoo.define('point_of_sale.devices', function (require) {
"use strict";

var core = require('web.core');
var Model = require('web.DataModel');
var Session = require('web.Session');

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

// the JobQueue schedules a sequence of 'jobs'. each job is
// a function returning a deferred. the queue waits for each job to finish
// before launching the next. Each job can also be scheduled with a delay.
// the  is used to prevent parallel requests to the proxy.

var JobQueue = function(){
    var queue = [];
    var running = false;
    var scheduled_end_time = 0;
    var end_of_queue = (new $.Deferred()).resolve();
    var stoprepeat = false;

    var run = function(){
        if(end_of_queue.state() === 'resolved'){
            end_of_queue =  new $.Deferred();
        }
        if(queue.length > 0){
            running = true;
            var job = queue[0];
            if(!job.opts.repeat || stoprepeat){
                queue.shift();
                stoprepeat = false;
            }

            // the time scheduled for this job
            scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);

            // we run the job and put in def when it finishes
            var def = job.fun() || (new $.Deferred()).resolve();

            // we don't care if a job fails ...
            def.always(function(){
                // we run the next job after the scheduled_end_time, even if it finishes before
                setTimeout(function(){
                    run();
                }, Math.max(0, scheduled_end_time - (new Date()).getTime()) );
            });
        }else{
            running = false;
            scheduled_end_time = 0;
            end_of_queue.resolve();
        }
    };

    // adds a job to the schedule.
    // opts : {
    //    duration    : the job is guaranteed to finish no quicker than this (milisec)
    //    repeat      : if true, the job will be endlessly repeated
    //    important   : if true, the scheduled job cannot be canceled by a queue.clear()
    // }
    this.schedule  = function(fun, opts){
        queue.push({fun:fun, opts:opts || {}});
        if(!running){
            run();
        }
    };

    // remove all jobs from the schedule (except the ones marked as important)
    this.clear = function(){
        queue = _.filter(queue,function(job){return job.opts.important === true;});
    };

    // end the repetition of the current job
    this.stoprepeat = function(){
        stoprepeat = true;
    };

    // returns a deferred that resolves when all scheduled
    // jobs have been run.
    // ( jobs added after the call to this method are considered as well )
    this.finished = function(){
        return end_of_queue;
    };

};


// this object interfaces with the local proxy to communicate to the various hardware devices
// connected to the Point of Sale. As the communication only goes from the POS to the proxy,
// methods are used both to signal an event, and to fetch information.

var ProxyDevice  = core.Class.extend(core.mixins.PropertiesMixin,{
    init: function(parent,options){
        core.mixins.PropertiesMixin.init.call(this,parent);
        var self = this;
        options = options || {};

        this.pos = parent;

        this.weighing = false;
        this.debug_weight = 0;
        this.use_debug_weight = false;

        this.paying = false;
        this.default_payment_status = {
            status: 'waiting',
            message: '',
            payment_method: undefined,
            receipt_client: undefined,
            receipt_shop:   undefined,
        };
        this.custom_payment_status = this.default_payment_status;

        this.receipt_queue = [];

        this.notifications = {};
        this.bypass_proxy = false;

        this.connection = null;
        this.host       = '';
        this.keptalive  = false;

        this.set('status',{});

        this.set_connection_status('disconnected');

        this.on('change:status',this,function(eh,status){
            status = status.newValue;
            if(status.status === 'connected'){
                self.print_receipt();
            }
        });

        window.hw_proxy = this;
    },
    set_connection_status: function(status,drivers){
        var oldstatus = this.get('status');
        var newstatus = {};
        newstatus.status = status;
        newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
        newstatus.drivers = drivers ? drivers : newstatus.drivers;
        this.set('status',newstatus);
    },
    disconnect: function(){
        if(this.get('status').status !== 'disconnected'){
            this.connection.destroy();
            this.set_connection_status('disconnected');
        }
    },

    // connects to the specified url
    connect: function(url){
        var self = this;
        this.connection = new Session(undefined,url, { use_cors: true});
        this.host   = url;
        this.set_connection_status('connecting',{});

        return this.message('handshake').then(function(response){
                if(response){
                    self.set_connection_status('connected');
                    localStorage.hw_proxy_url = url;
                    self.keepalive();
                }else{
                    self.set_connection_status('disconnected');
                    console.error('Connection refused by the Proxy');
                }
            },function(){
                self.set_connection_status('disconnected');
                console.error('Could not connect to the Proxy');
            });
    },

    // find a proxy and connects to it. for options see find_proxy
    //   - force_ip : only try to connect to the specified ip.
    //   - port: what port to listen to (default 8069)
    //   - progress(fac) : callback for search progress ( fac in [0,1] )
    autoconnect: function(options){
        var self = this;
        this.set_connection_status('connecting',{});
        var found_url = new $.Deferred();
        var success = new $.Deferred();

        if ( options.force_ip ){
            // if the ip is forced by server config, bailout on fail
            found_url = this.try_hard_to_connect(options.force_ip, options);
        }else if( localStorage.hw_proxy_url ){
            // try harder when we remember a good proxy url
            found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options)
                .then(null,function(){
                    return self.find_proxy(options);
                });
        }else{
            // just find something quick
            found_url = this.find_proxy(options);
        }

        success = found_url.then(function(url){
                return self.connect(url);
            });

        success.fail(function(){
            self.set_connection_status('disconnected');
        });

        return success;
    },

    // starts a loop that updates the connection status
    keepalive: function(){
        var self = this;

        function status(){
            self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})
                .then(function(driver_status){
                    self.set_connection_status('connected',driver_status);
                },function(){
                    if(self.get('status').status !== 'connecting'){
                        self.set_connection_status('disconnected');
                    }
                }).always(function(){
                    setTimeout(status,5000);
                });
        }

        if(!this.keptalive){
            this.keptalive = true;
            status();
        }
    },

    message : function(name,params){
        var callbacks = this.notifications[name] || [];
        for(var i = 0; i < callbacks.length; i++){
            callbacks[i](params);
        }
        if(this.get('status').status !== 'disconnected'){
            return this.connection.rpc('/hw_proxy/' + name, params || {});
        }else{
            return (new $.Deferred()).reject();
        }
    },

    // try several time to connect to a known proxy url
    try_hard_to_connect: function(url,options){
        options   = options || {};
        var port  = ':' + (options.port || '8069');

        this.set_connection_status('connecting');

        if(url.indexOf('//') < 0){
            url = 'http://'+url;
        }

        if(url.indexOf(':',5) < 0){
            url = url+port;
        }

        // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries
        function try_real_hard_to_connect(url, retries, done){

            done = done || new $.Deferred();

            $.ajax({
                url: url + '/hw_proxy/hello',
                method: 'GET',
                timeout: 1000,
            })
            .done(function(){
                done.resolve(url);
            })
            .fail(function(){
                if(retries > 0){
                    try_real_hard_to_connect(url,retries-1,done);
                }else{
                    done.reject();
                }
            });
            return done;
        }

        return try_real_hard_to_connect(url,3);
    },

    // returns as a deferred a valid host url that can be used as proxy.
    // options:
    //   - port: what port to listen to (default 8069)
    //   - progress(fac) : callback for search progress ( fac in [0,1] )
    find_proxy: function(options){
        options = options || {};
        var self  = this;
        var port  = ':' + (options.port || '8069');
        var urls  = [];
        var found = false;
        var parallel = 8;
        var done = new $.Deferred(); // will be resolved with the proxies valid urls
        var threads  = [];
        var progress = 0;


        urls.push('http://localhost'+port);
        for(var i = 0; i < 256; i++){
            urls.push('http://192.168.0.'+i+port);
            urls.push('http://192.168.1.'+i+port);
            urls.push('http://10.0.0.'+i+port);
        }

        var prog_inc = 1/urls.length;

        function update_progress(){
            progress = found ? 1 : progress + prog_inc;
            if(options.progress){
                options.progress(progress);
            }
        }

        function thread(done){
            var url = urls.shift();

            done = done || new $.Deferred();

            if( !url || found || !self.searching_for_proxy ){
                done.resolve();
                return done;
            }

            $.ajax({
                    url: url + '/hw_proxy/hello',
                    method: 'GET',
                    timeout: 400,
                }).done(function(){
                    found = true;
                    update_progress();
                    done.resolve(url);
                })
                .fail(function(){
                    update_progress();
                    thread(done);
                });

            return done;
        }

        this.searching_for_proxy = true;

        var len  = Math.min(parallel,urls.length);
        for(i = 0; i < len; i++){
            threads.push(thread());
        }

        $.when.apply($,threads).then(function(){
            var urls = [];
            for(var i = 0; i < arguments.length; i++){
                if(arguments[i]){
                    urls.push(arguments[i]);
                }
            }
            done.resolve(urls[0]);
        });

        return done;
    },

    stop_searching: function(){
        this.searching_for_proxy = false;
        this.set_connection_status('disconnected');
    },

    // this allows the client to be notified when a proxy call is made. The notification
    // callback will be executed with the same arguments as the proxy call
    add_notification: function(name, callback){
        if(!this.notifications[name]){
            this.notifications[name] = [];
        }
        this.notifications[name].push(callback);
    },

    // returns the weight on the scale.
    scale_read: function(){
        var self = this;
        var ret = new $.Deferred();
        if (self.use_debug_weight) {
            return (new $.Deferred()).resolve({weight:this.debug_weight, unit:'Kg', info:'ok'});
        }
        this.message('scale_read',{})
            .then(function(weight){
                ret.resolve(weight);
            }, function(){ //failed to read weight
                ret.resolve({weight:0.0, unit:'Kg', info:'ok'});
            });
        return ret;
    },

    // sets a custom weight, ignoring the proxy returned value.
    debug_set_weight: function(kg){
        this.use_debug_weight = true;
        this.debug_weight = kg;
    },

    // resets the custom weight and re-enable listening to the proxy for weight values
    debug_reset_weight: function(){
        this.use_debug_weight = false;
        this.debug_weight = 0;
    },

    // ask for the cashbox (the physical box where you store the cash) to be opened
    open_cashbox: function(){
        return this.message('open_cashbox');
    },

    /*
     * ask the printer to print a receipt
     */
    print_receipt: function(receipt){
        var self = this;
        if(receipt){
            this.receipt_queue.push(receipt);
        }
        function send_printing_job(){
            if (self.receipt_queue.length > 0){
                var r = self.receipt_queue.shift();
                self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 })
                    .then(function(){
                        send_printing_job();
                    },function(error){
                        if (error) {
                            self.pos.gui.show_popup('error-traceback',{
                                'title': _t('Printing Error: ') + error.data.message,
                                'body':  error.data.debug,
                            });
                            return;
                        }
                        self.receipt_queue.unshift(r);
                    });
            }
        }
        send_printing_job();
    },

    print_sale_details: function() { 
        var self = this;
        new Model('report.point_of_sale.report_saledetails').call('get_sale_details').then(function(result){
            var env = {
                company: self.pos.company,
                pos: self.pos,
                products: result.products,
                payments: result.payments,
                taxes: result.taxes,
                total_paid: result.total_paid,
                date: (new Date()).toLocaleString(),
            };
            var report = QWeb.render('SaleDetailsReport', env);
            self.print_receipt(report);
        })
    },

    // asks the proxy to log some information, as with the debug.log you can provide several arguments.
    log: function(){
        return this.message('log',{'arguments': _.toArray(arguments)});
    },

});

// this module interfaces with the barcode reader. It assumes the barcode reader
// is set-up to act like  a keyboard. Use connect() and disconnect() to activate
// and deactivate the barcode reader. Use set_action_callbacks to tell it
// what to do when it reads a barcode.
var BarcodeReader = core.Class.extend({
    actions:[
        'product',
        'cashier',
        'client',
    ],

    init: function(attributes){
        this.pos = attributes.pos;
        this.action_callback = {};
        this.proxy = attributes.proxy;
        this.remote_scanning = false;
        this.remote_active = 0;

        this.barcode_parser = attributes.barcode_parser;

        this.action_callback_stack = [];

        core.bus.on('barcode_scanned', this, function (barcode) {
            this.scan(barcode);
        });
    },

    set_barcode_parser: function(barcode_parser) {
        this.barcode_parser = barcode_parser;
    },

    save_callbacks: function(){
        var callbacks = {};
        for(var name in this.action_callback){
            callbacks[name] = this.action_callback[name];
        }
        this.action_callback_stack.push(callbacks);
    },

    restore_callbacks: function(){
        if(this.action_callback_stack.length){
            var callbacks = this.action_callback_stack.pop();
            this.action_callback = callbacks;
        }
    },

    // when a barcode is scanned and parsed, the callback corresponding
    // to its type is called with the parsed_barcode as a parameter.
    // (parsed_barcode is the result of parse_barcode(barcode))
    //
    // callbacks is a Map of 'actions' : callback(parsed_barcode)
    // that sets the callback for each action. if a callback for the
    // specified action already exists, it is replaced.
    //
    // possible actions include :
    // 'product' | 'cashier' | 'client' | 'discount'
    set_action_callback: function(action, callback){
        if(arguments.length == 2){
            this.action_callback[action] = callback;
        }else{
            var actions = arguments[0];
            for(var action in actions){
                this.set_action_callback(action,actions[action]);
            }
        }
    },

    //remove all action callbacks
    reset_action_callbacks: function(){
        for(var action in this.action_callback){
            this.action_callback[action] = undefined;
        }
    },

    scan: function(code){
        if (!code) {
            return;
        }
        var parsed_result = this.barcode_parser.parse_barcode(code);
        if (this.action_callback[parsed_result.type]) {
            this.action_callback[parsed_result.type](parsed_result);
        } else if (this.action_callback.error) {
            this.action_callback.error(parsed_result);
        } else {
            console.warn("Ignored Barcode Scan:", parsed_result);
        }
    },

    // the barcode scanner will listen on the hw_proxy/scanner interface for
    // scan events until disconnect_from_proxy is called
    connect_to_proxy: function(){
        var self = this;
        this.remote_scanning = true;
        if(this.remote_active >= 1){
            return;
        }
        this.remote_active = 1;

        function waitforbarcode(){
            return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500})
                .then(function(barcode){
                    if(!self.remote_scanning){
                        self.remote_active = 0;
                        return;
                    }
                    self.scan(barcode);
                    waitforbarcode();
                },
                function(){
                    if(!self.remote_scanning){
                        self.remote_active = 0;
                        return;
                    }
                    setTimeout(waitforbarcode,5000);
                });
        }
        waitforbarcode();
    },

    // the barcode scanner will stop listening on the hw_proxy/scanner remote interface
    disconnect_from_proxy: function(){
        this.remote_scanning = false;
    },
});

return {
    JobQueue: JobQueue,
    ProxyDevice: ProxyDevice,
    BarcodeReader: BarcodeReader,
};

});
Beispiel #20
0
odoo.define('web.CrashManager', function (require) {
"use strict";

var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');

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

var map_title ={
    user_error: _lt('Warning'),
    warning: _lt('Warning'),
    access_error: _lt('Access Error'),
    missing_error: _lt('Missing Record'),
    validation_error: _lt('Validation Error'),
    except_orm: _lt('Global Business Error'),
    access_denied: _lt('Access Denied'),
};

var CrashManager = core.Class.extend({
    init: function() {
        this.active = true;
    },
    enable: function () {
        this.active = true;
    },
    disable: function () {
        this.active = false;
    },
    rpc_error: function(error) {
        var self = this;
        if (!this.active) {
            return;
        }
        if (this.connection_lost) {
            return;
        }
        if (error.code === -32098) {
            core.bus.trigger('connection_lost');
            this.connection_lost = true;
            var timeinterval = setInterval(function() {
                ajax.jsonRpc('/web/webclient/version_info').then(function() {
                    clearInterval(timeinterval);
                    core.bus.trigger('connection_restored');
                    self.connection_lost = false;
                });
            }, 2000);
            return;
        }
        var handler = core.crash_registry.get(error.data.name, true);
        if (handler) {
            new (handler)(this, error).display();
            return;
        }
        if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
            this.show_warning({type: _t("Odoo Session Expired"), data: {message: _t("Your Odoo session expired. Please refresh the current web page.")}});
            return;
        }
        if (_.has(map_title, error.data.exception_type)) {
            if(error.data.exception_type === 'except_orm'){
                if(error.data.arguments[1]) {
                    error = _.extend({}, error,
                                {
                                    data: _.extend({}, error.data,
                                        {
                                            message: error.data.arguments[1],
                                            title: error.data.arguments[0] !== 'Warning' ? (" - " + error.data.arguments[0]) : '',
                                        })
                                });
                }
                else {
                    error = _.extend({}, error,
                                {
                                    data: _.extend({}, error.data,
                                        {
                                            message: error.data.arguments[0],
                                            title:  '',
                                        })
                                });
                }
            }
            else {
                error = _.extend({}, error,
                            {
                                data: _.extend({}, error.data,
                                    {
                                        message: error.data.arguments[0],
                                        title: map_title[error.data.exception_type] !== 'Warning' ? (" - " + map_title[error.data.exception_type]) : '',
                                    })
                            });
            }

            this.show_warning(error);
        //InternalError

        } else {
            this.show_error(error);
        }
    },
    show_warning: function(error) {
        if (!this.active) {
            return;
        }
        new Dialog(this, {
            size: 'medium',
            title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"),
            subtitle: error.data.title,
            $content: $(QWeb.render('CrashManager.warning', {error: error}))
        }).open();
    },
    show_error: function(error) {
        if (!this.active) {
            return;
        }
        var dialog = new Dialog(this, {
            title: _.str.capitalize(error.type || error.message) || _t("Odoo Error"),
            $content: $(QWeb.render('CrashManager.error', {error: error}))
        });

        // When the dialog opens, initialize the copy feature and destroy it when the dialog is closed
        var $clipboardBtn;
        var clipboard;
        dialog.opened(function () {
            // When the full traceback is shown, scroll it to the end (useful for better python error reporting)
            dialog.$(".o_error_detail").on("shown.bs.collapse", function (e) {
                e.target.scrollTop = e.target.scrollHeight;
            });

            $clipboardBtn = dialog.$(".o_clipboard_button");
            $clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"});
            clipboard = new window.Clipboard($clipboardBtn[0], {
                text: function () {
                    return (_t("Error") + ":\n" + error.message + "\n\n" + error.data.debug).trim();
                }
            });
            clipboard.on("success", function (e) {
                _.defer(function () {
                    $clipboardBtn.tooltip("show");
                    _.delay(function () {
                        $clipboardBtn.tooltip("hide");
                    }, 800);
                });
            });
        });
        dialog.on("closed", this, function () {
            $clipboardBtn.tooltip("destroy");
            clipboard.destroy();
        });

        dialog.open();
    },
    show_message: function(exception) {
        this.show_error({
            type: _t("Odoo Client Error"),
            message: exception,
            data: {debug: ""}
        });
    },
});

/**
 * An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
*/
var ExceptionHandler = {
    /**
     * @param parent The parent.
     * @param error The error object as returned by the JSON-RPC implementation.
     */
    init: function(parent, error) {},
    /**
     * Called to inform to display the widget, if necessary. A typical way would be to implement
     * this interface in a class extending instance.web.Dialog and simply display the dialog in this
     * method.
     */
    display: function() {},
};


/**
 * Handle redirection warnings, which behave more or less like a regular
 * warning, with an additional redirection button.
 */
var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
    init: function(parent, error) {
        this._super(parent);
        this.error = error;
    },
    display: function() {
        var self = this;
        var error = this.error;
        error.data.message = error.data.arguments[0];

        new Dialog(this, {
            size: 'medium',
            title: _.str.capitalize(error.type) || _t("Odoo Warning"),
            buttons: [
                {text: error.data.arguments[2], classes : "btn-primary", click: function() {
                    window.location.href = '#action='+error.data.arguments[1];
                    self.destroy();
                }},
                {text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
            ],
            $content: QWeb.render('CrashManager.warning', {error: error}),
        }).open();
    }
});

core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandler);

return CrashManager;
});
odoo.define('website.snippets.animation', function (require) {
'use strict';

var ajax = require('web.ajax');
var core = require('web.core');
var website = require('website.website');

var readyAnimation = [];

var animationRegistry = Object.create(null);

function load_called_template () {
    var ids_or_xml_ids = _.uniq($("[data-oe-call]").map(function () {return $(this).data('oe-call');}).get());
    if (ids_or_xml_ids.length) {
        ajax.jsonRpc('/website/multi_render', 'call', {
                'ids_or_xml_ids': ids_or_xml_ids
            }).then(function (data) {
                for (var k in data) {
                    var $data = $(data[k]).addClass('o_block_'+k);
                    $("[data-oe-call='"+k+"']").each(function () {
                        $(this).replaceWith($data.clone());
                    });
                }
            });
    }
}

function start_animation(editable_mode, $target) {
    for (var k in animationRegistry) {
        var Animation = animationRegistry[k];
        var selector = "";
        if (Animation.prototype.selector) {
            if (selector !== "") selector += ", ";
            selector += Animation.prototype.selector;
        }
        if ($target) {
            if ($target.is(selector)) selector = $target;
            else continue;
        }

        $(selector).each(function () {
            var $snipped_id = $(this);
            if (    !$snipped_id.parents("#oe_snippets").length &&
                    !$snipped_id.parent("body").length &&
                    !$snipped_id.data("snippet-view")) {
                readyAnimation.push($snipped_id);
                $snipped_id.data("snippet-view", new Animation($snipped_id, editable_mode));
            } else if ($snipped_id.data("snippet-view")) {
                $snipped_id.data("snippet-view").start(editable_mode);
            }
        });
    }
}

function stop_animation() {
    $(readyAnimation).each(function() {
        var $snipped_id = $(this);
        if ($snipped_id.data("snippet-view")) {
            $snipped_id.data("snippet-view").stop();
        }
    });
}

$(document).ready(function () {
    load_called_template(); // if asset is placed into head, move this call into $(document).ready

    if ($(".o_gallery:not(.oe_slideshow)").size()) {
        // load gallery modal template
        website.add_template_file('/website/static/src/xml/website.gallery.xml');
    }

    start_animation();
});



var Animation = core.Class.extend({
    selector: false,
    $: function () {
        return this.$el.find.apply(this.$el, arguments);
    },
    init: function (dom, editable_mode) {
        this.$el = this.$target = $(dom);
        this.start(editable_mode);
    },
    /*
    *  start
    *  This method is called after init
    */
    start: function () {
    },
    /*
    *  stop
    *  This method is called to stop the animation (e.g.: when rte is launch)
    */
    stop: function () {
    },
});

animationRegistry.slider = Animation.extend({
    selector: ".carousel",
    start: function () {
        this.$target.carousel();
    },
    stop: function () {
        this.$target.carousel('pause');
        this.$target.removeData("bs.carousel");
    },
});

animationRegistry.parallax = Animation.extend({
    selector: ".parallax",
    start: function () {
        var self = this;
        setTimeout(function () {self.set_values();});
        this.on_scroll = function () {
            var speed = parseFloat(self.$target.attr("data-scroll-background-ratio") || 0);
            if (speed == 1) return;
            var offset = parseFloat(self.$target.attr("data-scroll-background-offset") || 0);
            var top = offset + window.scrollY * speed;
            self.$target.css("background-position", "0px " + top + "px");
        };
        this.on_resize = function () {
            self.set_values();
        };
        $(window).on("scroll", this.on_scroll);
        $(window).on("resize", this.on_resize);
    },
    stop: function () {
        $(window).off("scroll", this.on_scroll)
                .off("resize", this.on_resize);
    },
    set_values: function () {
        var self = this;
        var speed = parseFloat(self.$target.attr("data-scroll-background-ratio") || 0);

        if (speed === 1 || this.$target.css("background-image") === "none") {
            this.$target.css("background-attachment", "fixed").css("background-position", "0px 0px");
            return;
        } else {
            this.$target.css("background-attachment", "scroll");
        }

        this.$target.attr("data-scroll-background-offset", 0);
        var img = new Image();
        img.onload = function () {
            var offset = 0;
            var padding =  parseInt($(document.body).css("padding-top"));
            if (speed > 1) {
                var inner_offset = - self.$target.outerHeight() + this.height / this.width * document.body.clientWidth;
                var outer_offset = self.$target.offset().top - (document.body.clientHeight - self.$target.outerHeight()) - padding;
                offset = - outer_offset * speed + inner_offset;
            } else {
                offset = - self.$target.offset().top * speed;
            }
            self.$target.attr("data-scroll-background-offset", offset > 0 ? 0 : offset);
            $(window).scroll();
        };
        img.src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)/g, "");
        $(window).scroll();
    }
});

animationRegistry.share = Animation.extend({
    selector: ".oe_share",
    start: function () {
        var url = encodeURIComponent(window.location.href);
        var title = encodeURIComponent($("title").text());
        this.$("a").each(function () {
            var $a = $(this);
            var url_regex = /\{url\}|%7Burl%7D/, title_regex = /\{title\}|%7Btitle%7D/;
            $a.attr("href", $(this).attr("href").replace(url_regex, url).replace(title_regex, title));
            if ($a.attr("target") && $a.attr("target").match(/_blank/i) && !$a.closest('.o_editable').length) {
                $a.on('click', function () {
                    window.open(this.href,'','menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=550,width=600');
                    return false;
                });
            }
        });
    }
});

animationRegistry.media_video = Animation.extend({
    selector: ".media_iframe_video",
    start: function () {
        if (!this.$target.has('.media_iframe_video_size')) {
            var editor = '<div class="css_editable_mode_display">&nbsp;</div>';
            var size = '<div class="media_iframe_video_size">&nbsp;</div>';
            this.$target.html(editor+size+'<iframe src="'+this.$target.data("src")+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
        }
    },
});

animationRegistry.ul = Animation.extend({
    selector: "ul.o_ul_folded, ol.o_ul_folded",
    start: function () {
        this.$('.o_ul_toggle_self').off('click').on('click', function (event) {
            $(this).toggleClass('o_open');
            $(this).closest('li').find('ul,ol').toggleClass('o_close');
            event.preventDefault();
        });

        this.$('.o_ul_toggle_next').off('click').on('click', function (event) {
            $(this).toggleClass('o_open');
            $(this).closest('li').next().toggleClass('o_close');
            event.preventDefault();
        });
    },
});

/* -------------------------------------------------------------------------
Gallery Animation  

This ads a Modal window containing a slider when an image is clicked 
inside a gallery 
-------------------------------------------------------------------------*/
animationRegistry.gallery = Animation.extend({
    selector: ".o_gallery:not(.o_slideshow)",
    start: function() {
        this.$el.on("click", "img", this.click_handler);
    },
    click_handler : function(event) {
        var $cur = $(event.currentTarget);
        var edition_mode = ($cur.closest("[contenteditable='true']").size() !== 0);
        
        // show it only if not in edition mode
        if (!edition_mode) {
            var urls = [],
                idx = undefined,
                milliseconds = undefined,
                params = undefined,
                $images = $cur.closest(".o_gallery").find("img"),
                size = 0.8,
                dimensions = {
                    min_width  : Math.round( window.innerWidth  *  size*0.9),
                    min_height : Math.round( window.innerHeight *  size),
                    max_width  : Math.round( window.innerWidth  *  size*0.9),
                    max_height : Math.round( window.innerHeight *  size),
                    width : Math.round( window.innerWidth *  size*0.9),
                    height : Math.round( window.innerHeight *  size)
            };

            $images.each(function() {
                urls.push($(this).attr("src"));
            });
            var $img = ($cur.is("img") === true) ? $cur : $cur.closest("img");
            idx = urls.indexOf($img.attr("src"));

            milliseconds = $cur.closest(".o_gallery").data("interval") || false;
            params = {
                srcs : urls,
                index: idx,
                dim  : dimensions,
                interval : milliseconds,
                id: _.uniqueId("slideshow_")
            };
            var $modal = $(core.qweb.render('website.gallery.slideshow.lightbox', params));
            $modal.modal({
                keyboard : true,
                backdrop : true
            });
            $modal.on('hidden.bs.modal', function() {
                $(this).hide();
                $(this).siblings().filter(".modal-backdrop").remove(); // bootstrap leaves a modal-backdrop
                $(this).remove();

            });
            $modal.find(".modal-content, .modal-body.o_slideshow").css("height", "100%");
            $modal.appendTo(document.body);

            this.carousel = new animationRegistry.gallery_slider($modal.find(".carousel").carousel());
        }
    } // click_handler  
});

animationRegistry.gallery_slider = Animation.extend({
    selector: ".o_slideshow",
    start: function() {
        var $carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel");
        var $indicator = $carousel.find('.carousel-indicators');
        var $lis = $indicator.find('li:not(.fa)');
        var $prev = $indicator.find('li.fa:first');
        var $next = $indicator.find('li.fa:last');
        var index = ($lis.filter('.active').index() || 1) -1;
        var page = Math.floor(index / 10);
        var nb = Math.ceil($lis.length / 10);

         // fix bootstrap use index insead of data-slide-to
        $carousel.on('slide.bs.carousel', function() {
            setTimeout(function () {
                var $item = $carousel.find('.carousel-inner .prev, .carousel-inner .next');
                var index = $item.index();
                $lis.removeClass("active")
                    .filter('[data-slide-to="'+index+'"]')
                    .addClass("active");
            },0);
        });

        function hide () {
            $lis.addClass('hidden').each(function (i) {
                if (i >= page*10 && i < (page+1)*10) {
                    $(this).removeClass('hidden');
                }
            });
            $prev.css('visibility', page === 0 ? 'hidden' : '');
            $next.css('visibility', (page+1) >= nb ? 'hidden' : '');
        }

        $indicator.find('li.fa').on('click', function () {
            page = (page + ($(this).hasClass('o_indicators_left')?-1:1)) % nb;
            $carousel.carousel(page*10);
            hide();
        });
        hide();

        $carousel.on('slid.bs.carousel', function() {
            var index = ($lis.filter('.active').index() || 1) -1;
            page = Math.floor(index / 10);
            hide();
        });
    }
});

return {
    Animation: Animation,
    readyAnimation: readyAnimation,
    start_animation: start_animation,
    stop_animation: stop_animation,
    registry: animationRegistry,
};

});
Beispiel #22
0
odoo.define('web.DataManager', function (require) {
"use strict";

var config = require('web.config');
var core = require('web.core');
var fieldRegistry = require('web.field_registry');
var pyeval = require('web.pyeval');
var session = require('web.session');
var utils = require('web.utils');

return core.Class.extend({
    init: function () {
        this._init_cache();
    },

    _init_cache: function () {
        this._cache = {
            actions: {},
            fields_views: {},
            filters: {},
            views: {},
        };
    },

    /**
     * Invalidates the whole cache
     * Suggestion: could be refined to invalidate some part of the cache
     */
    invalidate: function () {
        this._init_cache();
    },

    /**
     * Loads an action from its id or xmlid.
     *
     * @param {int|string} [action_id] the action id or xmlid
     * @param {Object} [additional_context] used to load the action
     * @return {Deferred} resolved with the action whose id or xmlid is action_id
     */
    load_action: function (action_id, additional_context) {
        var self = this;
        var key = this._gen_key(action_id, additional_context || {});

        if (!this._cache.actions[key]) {
            this._cache.actions[key] = session.rpc("/web/action/load", {
                action_id: action_id,
                additional_context : additional_context,
            }).then(function (action) {
                self._cache.actions[key] = action.no_cache ? null : self._cache.actions[key];
                return action;
            }, this._invalidate.bind(this, this._cache.actions, key));
        }

        return this._cache.actions[key].then(function (action) {
            return $.extend(true, {}, action);
        });
    },

    /**
     * Loads various information concerning views: fields_view for each view,
     * the fields of the corresponding model, and optionally the filters.
     *
     * @param {Object} [dataset] the dataset for which the views are loaded
     * @param {Array} [views_descr] array of [view_id, view_type]
     * @param {Object} [options] dictionnary of various options:
     *     - options.load_filters: whether or not to load the filters,
     *     - options.action_id: the action_id (required to load filters),
     *     - options.toolbar: whether or not a toolbar will be displayed,
     * @return {Deferred} resolved with the requested views information
     */
    load_views: function (params, options) {
        var self = this;

        var model = params.model;
        var context = params.context;
        var views_descr = params.views_descr;
        var key = this._gen_key(model, views_descr, options || {}, context);

        if (!this._cache.views[key]) {
            // Don't load filters if already in cache
            var filters_key;
            if (options.load_filters) {
                filters_key = this._gen_key(model, options.action_id);
                options.load_filters = !this._cache.filters[filters_key];
            }

            this._cache.views[key] = session.rpc('/web/dataset/call_kw/' + model + '/load_views', {
                args: [],
                kwargs: {
                    views: views_descr,
                    options: options,
                    context: context.eval(),
                },
                model: model,
                method: 'load_views',
            }).then(function (result) {
                // Postprocess fields_views and insert them into the fields_views cache
                result.fields_views = _.mapObject(result.fields_views, self._postprocess_fvg.bind(self));
                self.processViews(result.fields_views, result.fields);
                _.each(views_descr, function (view_descr) {
                    var toolbar = options.toolbar && view_descr[1] !== 'search';
                    var fv_key = self._gen_key(model, view_descr[0], view_descr[1], toolbar, context);
                    self._cache.fields_views[fv_key] = $.when(result.fields_views[view_descr[1]]);
                });

                // Insert filters, if any, into the filters cache
                if (result.filters) {
                    self._cache.filters[filters_key] = $.when(result.filters);
                }

                return result.fields_views;
            }, this._invalidate.bind(this, this._cache.views, key));
        }

        return this._cache.views[key];
    },

    /**
     * Loads the filters of a given model and optional action id.
     *
     * @param {Object} [dataset] the dataset for which the filters are loaded
     * @param {int} [action_id] the id of the action (optional)
     * @return {Deferred} resolved with the requested filters
     */
    load_filters: function (dataset, action_id) {
        var key = this._gen_key(dataset.model, action_id);
        if (!this._cache.filters[key]) {
            this._cache.filters[key] = session.rpc('/web/dataset/call_kw/ir.filters/get_filters', {
                args: [dataset.model, action_id],
                kwargs: {
                    context: dataset.get_context(),
                },
                model: 'ir.filters',
                method: 'get_filters',
            }).fail(this._invalidate.bind(this, this._cache.filters, key));
        }
        return this._cache.filters[key];
    },

    /**
     * Calls 'create_or_replace' on 'ir_filters'.
     *
     * @param {Object} [filter] the filter description
     * @return {Deferred} resolved with the id of the created or replaced filter
     */
    create_filter: function (filter) {
        var self = this;
        return session.rpc('/web/dataset/call_kw/ir.filters/create_or_replace', {
                args: [filter],
                model: 'ir.filters',
                method: 'create_or_replace',
            })
            .then(function (filter_id) {
                var key = [
                    filter.model_id,
                    filter.action_id || false,
                ].join(',');
                self._invalidate(self._cache.filters, key);
                return filter_id;
            });
    },

    /**
     * Calls 'unlink' on 'ir_filters'.
     *
     * @param {Object} [filter] the description of the filter to remove
     * @return {Deferred}
     */
    delete_filter: function (filter) {
        var self = this;
        return session.rpc('/web/dataset/call_kw/ir.filters/unlink', {
                args: [filter.id],
                model: 'ir.filters',
                method: 'unlink',
            })
            .then(function () {
                self._cache.filters = {}; // invalidate cache
            });
    },

    /**
     * Processes fields and fields_views. For each field, writes its name inside
     * the field description to make it self-contained. For each fields_view,
     * completes its fields with the missing ones.
     *
     * @param {Object} fieldsViews object of fields_views (keys are view types)
     * @param {Object} fields all the fields of the model
     */
    processViews: function (fieldsViews, fields) {
        var fieldName, fieldsView, viewType;
        // write the field name inside the description for all fields
        for (fieldName in fields) {
            fields[fieldName].name = fieldName;
        }
        for (viewType in fieldsViews) {
            fieldsView = fieldsViews[viewType];
            // write the field name inside the description for fields in view
            for (fieldName in fieldsView.fields) {
                fieldsView.fields[fieldName].name = fieldName;
            }
            // complete fields (in view) with missing ones
            _.defaults(fieldsView.fields, fields);
            // process the fields_view
            _.extend(fieldsView, this._processFieldsView({
                type: viewType,
                arch: fieldsView.arch,
                fields: fieldsView.fields,
            }));
        }
    },

    /**
     * Private function that postprocesses fields_view (mainly parses the arch attribute)
     */
    _postprocess_fvg: function (fields_view) {
        var self = this;

        // Parse arch
        var doc = $.parseXML(fields_view.arch).documentElement;
        fields_view.arch = utils.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));

        // Process inner views (x2manys)
        _.each(fields_view.fields, function(field) {
            _.each(field.views || {}, function(view) {
                self._postprocess_fvg(view);
            });
        });

        return fields_view;
    },

    /**
     * Private function that generates a cache key from its arguments
     */
    _gen_key: function () {
        return _.map(Array.prototype.slice.call(arguments), function (arg) {
            if (!arg) {
                return false;
            }
            return _.isObject(arg) ? JSON.stringify(arg) : arg;
        }).join(',');
    },

    /**
     * Private function that invalidates a cache entry
     */
    _invalidate: function (cache, key) {
        delete cache[key];
    },

    ///////////////////////////////////////////////////////////////

    /**
     * Process a field node, in particular, put a flag on the field to give
     * special directives to the BasicModel.
     *
     * @param {string} viewType
     * @param {Object} field - the field properties
     * @param {Object} attrs - the field attributes (from the xml)
     * @returns {Object} attrs
     */
    _processField: function (viewType, field, attrs) {
        var self = this;
        attrs.Widget = this._getFieldWidgetClass(viewType, field, attrs);

        if (!_.isObject(attrs.options)) { // parent arch could have already been processed (TODO this should not happen)
            attrs.options = attrs.options ? pyeval.py_eval(attrs.options) : {};
        }

        if (attrs.on_change && !field.onChange) {
            field.onChange = "1";
        }

        if (!_.isEmpty(field.views)) {
            // process the inner fields_view as well to find the fields they use.
            // register those fields' description directly on the view.
            // for those inner views, the list of all fields isn't necessary, so
            // basically the field_names will be the keys of the fields obj.
            // don't use _ to iterate on fields in case there is a 'length' field,
            // as _ doesn't behave correctly when there is a length key in the object
            attrs.views = {};
            _.each(field.views, function (innerFieldsView, viewType) {
                viewType = viewType === 'tree' ? 'list' : viewType;
                innerFieldsView.type = viewType;
                attrs.views[viewType] = self._processFieldsView(_.extend({}, innerFieldsView));
            });
            delete field.views;
        }

        if (field.type === 'one2many' || field.type === 'many2many') {
            if (attrs.Widget.prototype.useSubview) {
                if (!attrs.views) {
                    attrs.views = {};
                }
                var mode = attrs.mode;
                if (!mode) {
                    if (attrs.views.tree && attrs.views.kanban) {
                        mode = 'tree';
                    } else if (!attrs.views.tree && attrs.views.kanban) {
                        mode = 'kanban';
                    } else {
                        mode = 'tree,kanban';
                    }
                }
                if (mode.indexOf(',') !== -1) {
                    mode = config.device.size_class !== config.device.SIZES.XS ? 'tree' : 'kanban';
                }
                if (mode === 'tree') {
                    mode = 'list';
                    if (!attrs.views.list && attrs.views.tree) {
                        attrs.views.list = attrs.views.tree;
                    }
                }
                attrs.mode = mode;
            }
            if (attrs.Widget.prototype.fetchSubFields) {
                attrs.relatedFields = {
                    display_name: {type: 'char'},
                    //id: {type: 'integer'},
                };
                attrs.fieldsInfo = {};
                attrs.fieldsInfo.default = {display_name: {}, id: {}};
                attrs.viewType = 'default';
                if (attrs.color || 'color') {
                    attrs.relatedFields[attrs.color || 'color'] = {type: 'integer'};
                    attrs.fieldsInfo.default.color = {};
                }
            }
        }
        return attrs;
    },
    /**
     * Visit all nodes in the arch field and process each fields
     *
     * @param {string} viewType
     * @param {Object} arch
     * @param {Object} fields
     * @returns {Object} fieldsInfo
     */
    _processFields: function (viewType, arch, fields) {
        var self = this;
        var fieldsInfo = Object.create(null);
        utils.traverse(arch, function (node) {
            if (typeof node === 'string') {
                return false;
            }
            if (node.tag === 'field') {
                fieldsInfo[node.attrs.name] = self._processField(viewType,
                    fields[node.attrs.name], node.attrs ? _.clone(node.attrs) : {});
                return false;
            }
            return node.tag !== 'arch';
        });
        return fieldsInfo;
    },
    /**
     * Visit all nodes in the arch field and process each fields and inner views
     *
     * @param {Object} viewInfo
     * @param {Object} viewInfo.arch
     * @param {Object} viewInfo.fields
     * @returns {Object} viewInfo
     */
    _processFieldsView: function (viewInfo) {
        var viewFields = this._processFields(viewInfo.type, viewInfo.arch, viewInfo.fields);
        // by default fetch display_name and id
        if (!viewInfo.fields.display_name) {
            viewInfo.fields.display_name = {type: 'char'};
            viewFields.display_name = {};
        }
        viewInfo.fieldsInfo = {};
        viewInfo.fieldsInfo[viewInfo.type] = viewFields;
        utils.deepFreeze(viewInfo.fields);
        return viewInfo;
    },
    /**
     * Returns the AbstractField specialization that should be used for the
     * given field informations. If there is no mentioned specific widget to
     * use, determine one according the field type.
     *
     * @param {string} viewType
     * @param {Object} field
     * @param {Object} attrs
     * @returns {function|null} AbstractField specialization Class
     */
    _getFieldWidgetClass: function (viewType, field, attrs) {
        var Widget;
        if (attrs.widget) {
            Widget = fieldRegistry.getAny([viewType + "." + attrs.widget, attrs.widget]);
            if (!Widget) {
                console.warn("Missing widget: ", attrs.widget, " for field", attrs.name, "of type", field.type);
            }
        } else if (viewType === 'kanban' && field.type === 'many2many') {
            // we want to display the widget many2manytags in kanban even if it
            // is not specified in the view
            Widget = fieldRegistry.get('kanban.many2many_tags');
        }
        return Widget || fieldRegistry.getAny([viewType + "." + field.type, field.type, "abstract"]);
    },
});

});
Beispiel #23
0
odoo.define('point_of_sale.gui', function (require) {
"use strict";
// this file contains the Gui, which is the pos 'controller'. 
// It contains high level methods to manipulate the interface
// such as changing between screens, creating popups, etc.
//
// it is available to all pos objects trough the '.gui' field.

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

var _t = core._t;

var Gui = core.Class.extend({
    screen_classes: [],
    popup_classes:  [],
    init: function(options){
        var self = this;
        this.pos            = options.pos;
        this.chrome         = options.chrome;
        this.screen_instances     = {};
        this.popup_instances      = {};
        this.default_screen = null;
        this.startup_screen = null;
        this.current_popup  = null;
        this.current_screen = null; 

        this.chrome.ready.then(function(){
            var order = self.pos.get_order();
            if (order) {
                self.show_saved_screen(order);
            } else {
                self.show_screen(self.startup_screen);
            }
            self.pos.bind('change:selectedOrder', function(){
                self.show_saved_screen(self.pos.get_order());
            });
        });
    },

    /* ---- Gui: SCREEN MANIPULATION ---- */

    // register a screen widget to the gui,
    // it must have been inserted into the dom.
    add_screen: function(name, screen){
        screen.hide();
        this.screen_instances[name] = screen;
    },

    // sets the screen that will be displayed
    // for new orders
    set_default_screen: function(name){ 
        this.default_screen = name;
    },

    // sets the screen that will be displayed
    // when no orders are present
    set_startup_screen: function(name) {
        this.startup_screen = name;
    },

    // display the screen saved in an order,
    // called when the user changes the current order
    // no screen saved ? -> display default_screen
    // no order ? -> display startup_screen
    show_saved_screen:  function(order,options) {
        options = options || {};
        this.close_popup();
        if (order) {
            this.show_screen(order.get_screen_data('screen') || 
                             options.default_screen || 
                             this.default_screen,
                             null,'refresh');
        } else {
            this.show_screen(this.startup_screen);
        }
    },

    // display a screen. 
    // If there is an order, the screen will be saved in the order
    // - params: used to load a screen with parameters, for
    // example loading a 'product_details' screen for a specific product.
    // - refresh: if you want the screen to cycle trough show / hide even
    // if you are already on the same screen.
    show_screen: function(screen_name,params,refresh) {
        var screen = this.screen_instances[screen_name];
        if (!screen) {
            console.error("ERROR: show_screen("+screen_name+") : screen not found");
        }

        this.close_popup();

        var order = this.pos.get_order();
        if (order) {
            var old_screen_name = order.get_screen_data('screen');

            order.set_screen_data('screen',screen_name);

            if(params){
                order.set_screen_data('params',params);
            }

            if( screen_name !== old_screen_name ){
                order.set_screen_data('previous-screen',old_screen_name);
            }
        }

        if (refresh || screen !== this.current_screen) {
            if (this.current_screen) {
                this.current_screen.close();
                this.current_screen.hide();
            }
            this.current_screen = screen;
            this.current_screen.show();
        }
    },
    
    // returns the current screen.
    get_current_screen: function() {
        return this.pos.get_order() ? ( this.pos.get_order().get_screen_data('screen') || this.default_screen ) : this.startup_screen;
    },

    // goes to the previous screen (as specified in the order). The history only
    // goes 1 deep ...
    back: function() {
        var previous = this.pos.get_order().get_screen_data('previous-screen');
        if (previous) {
            this.show_screen(previous);
        }
    },

    // returns the parameter specified when this screen was displayed
    get_current_screen_param: function(param) {
        if (this.pos.get_order()) {
            var params = this.pos.get_order().get_screen_data('params');
            return params ? params[param] : undefined;
        } else {
            return undefined;
        }
    },

    /* ---- Gui: POPUP MANIPULATION ---- */

    // registers a new popup in the GUI.
    // the popup must have been previously inserted
    // into the dom.
    add_popup: function(name, popup) {
        popup.hide();
        this.popup_instances[name] = popup;
    },

    // displays a popup. Popup do not stack,
    // are not remembered by the order, and are
    // closed by screen changes or new popups.
    show_popup: function(name,options) {
        if (this.current_popup) {
            this.close_popup();
        }
        this.current_popup = this.popup_instances[name];
        this.current_popup.show(options);
    },

    // close the current popup.
    close_popup: function() {
        if  (this.current_popup) {
            this.current_popup.close();
            this.current_popup.hide();
            this.current_popup = null;
        }
    },

    // is there an active popup ?
    has_popup: function() {
        return !!this.current_popup;
    },

    /* ---- Gui: ACCESS CONTROL ---- */

    // A Generic UI that allow to select a user from a list.
    // It returns a deferred that resolves with the selected user 
    // upon success. Several options are available :
    // - security: passwords will be asked
    // - only_managers: restricts the list to managers
    // - current_user: password will not be asked if this 
    //                 user is selected.
    // - title: The title of the user selection list. 
    select_user: function(options){
        options = options || {};
        var self = this;
        var def  = new $.Deferred();

        var list = [];
        for (var i = 0; i < this.pos.users.length; i++) {
            var user = this.pos.users[i];
            if (!options.only_managers || user.role === 'manager') {
                list.push({
                    'label': user.name,
                    'item':  user,
                });
            }
        }

        this.show_popup('selection',{
            'title': options.title || _t('Select User'),
            list: list,
            confirm: function(user){ def.resolve(user); },
            cancel:  function(){ def.reject(); },
        });

        return def.then(function(user){
            if (options.security && user !== options.current_user && user.pos_security_pin) {
                return self.ask_password(user.pos_security_pin).then(function(){
                    return user;
                });
            } else {
                return user;
            }
        });
    },

    // Ask for a password, and checks if it this
    // the same as specified by the function call.
    // returns a deferred that resolves on success,
    // fails on failure.
    ask_password: function(password) {
        var self = this;
        var ret = new $.Deferred();
        if (password) {
            this.gui.show_popup('password',{
                'title': _t('Password ?'),
                confirm: function(pw) {
                    if (pw !== password) {
                        self.show_popup('error',_t('Incorrect Password'));
                        ret.reject();
                    } else {
                        ret.resolve();
                    }
                },
            });
        } else {
            ret.resolve();
        }
        return ret;
    },

    // checks if the current user (or the user provided) has manager
    // access rights. If not, a popup is shown allowing the user to
    // temporarily login as an administrator. 
    // This method returns a deferred, that succeeds with the 
    // manager user when the login is successfull.
    sudo: function(user){
        user = user || this.pos.get_cashier();

        if (user.role === 'manager') {
            return new $.Deferred().resolve(user);
        } else {
            return this.select_user({
                security:       true, 
                only_managers:  true,
                title:       _t('Login as a Manager'),
            });
        }
    },

    /* ---- Gui: CLOSING THE POINT OF SALE ---- */

    close: function() {
        var self = this;
        this.chrome.loading_show();
        this.chrome.loading_message(_t('Closing ...'));

        this.pos.push_order().then(function(){
            return new Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id'])
            .pipe(function(res) {
                window.location = '/web#action=' + res[0]['res_id'];
            },function(err,event) {
                event.preventDefault();
                self.show_popup('error',{
                    'title': _t('Could not close the point of sale.'),
                    'body':  _t('Your internet connection is probably down.'),
                });
                self.chrome.widget.close_button.renderElement();
            });
        });
    },

    /* ---- Gui: SOUND ---- */

    play_sound: function(sound) {
        var src = '';
        if (sound === 'error') {
            src = "/point_of_sale/static/src/sounds/error.wav";
        } else if (sound === 'bell') {
            src = "/point_of_sale/static/src/sounds/bell.wav";
        } else {
            console.error('Unknown sound: ',sound);
            return;
        }
        $('body').append('<audio src="'+src+'" autoplay="true"></audio>');
    },

    /* ---- Gui: KEYBOARD INPUT ---- */

    // This is a helper to handle numpad keyboard input. 
    // - buffer: an empty or number string
    // - input:  '[0-9],'+','-','.','CLEAR','BACKSPACE'
    // - options: 'firstinput' -> will clear buffer if
    //     input is '[0-9]' or '.'
    //  returns the new buffer containing the modifications
    //  (the original is not touched) 
    numpad_input: function(buffer, input, options) { 
        var newbuf  = buffer.slice(0);
        options = options || {};

        if (input === '.') {
            if (options.firstinput) {
                newbuf = "0.";
            }else if (!newbuf.length || newbuf === '-') {
                newbuf += "0.";
            } else if (newbuf.indexOf('.') < 0){
                newbuf = newbuf + '.';
            }
        } else if (input === 'CLEAR') {
            newbuf = ""; 
        } else if (input === 'BACKSPACE') { 
            newbuf = newbuf.substring(0,newbuf.length - 1);
        } else if (input === '+') {
            if ( newbuf[0] === '-' ) {
                newbuf = newbuf.substring(1,newbuf.length);
            }
        } else if (input === '-') {
            if ( newbuf[0] === '-' ) {
                newbuf = newbuf.substring(1,newbuf.length);
            } else {
                newbuf = '-' + newbuf;
            }
        } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
            newbuf = '' + ((parseFloat(newbuf) || 0) + parseFloat(input));
        } else if (!isNaN(parseInt(input))) {
            if (options.firstinput) {
                newbuf = '' + input;
            } else {
                newbuf += input;
            }
        }

        // End of input buffer at 12 characters.
        if (newbuf.length > buffer.length && newbuf.length > 12) {
            this.play_sound('bell');
            return buffer.slice(0);
        }

        return newbuf;
    },
});

var define_screen = function (classe) {
    Gui.prototype.screen_classes.push(classe);
};

var define_popup = function (classe) {
    Gui.prototype.popup_classes.push(classe);
};

return {
    Gui: Gui,
    define_screen: define_screen,
    define_popup: define_popup,
};

});
Beispiel #24
0
odoo.define('barcodes.BarcodeEvents', function(require) {
"use strict";

var core = require('web.core');
var mixins = require('web.mixins');
var session = require('web.session');


// For IE >= 9, use this, new CustomEvent(), instead of new Event()
function CustomEvent ( event, params ) {
    params = params || { bubbles: false, cancelable: false, detail: undefined };
    var evt = document.createEvent( 'CustomEvent' );
    evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
    return evt;
   }
CustomEvent.prototype = window.Event.prototype;

var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
    timeout: null,
    key_pressed: {},
    buffered_key_events: [],
    // Regexp to match a barcode input and extract its payload
    // Note: to build in init() if prefix/suffix can be configured
    regexp: /(.{3,})[\n\r\t]*/,
    // By knowing the terminal character we can interpret buffered keys
    // as a barcode as soon as it's encountered (instead of waiting x ms)
    suffix: /[\n\r\t]+/,
    // Keys from a barcode scanner are usually processed as quick as possible,
    // but some scanners can use an intercharacter delay (we support <= 50 ms)
    max_time_between_keys_in_ms: session.max_time_between_keys_in_ms || 55,

    init: function() {
        mixins.PropertiesMixin.init.call(this);
        // Keep a reference of the handler functions to use when adding and removing event listeners
        this.__keydown_handler = _.bind(this.keydown_handler, this);
        this.__keyup_handler = _.bind(this.keyup_handler, this);
        this.__handler = _.bind(this.handler, this);
        // Bind event handler once the DOM is loaded
        // TODO: find a way to be active only when there are listeners on the bus
        $(_.bind(this.start, this, false));
    },

    handle_buffered_keys: function() {
        var str = this.buffered_key_events.reduce(function(memo, e) { return memo + String.fromCharCode(e.which) }, '');
        var match = str.match(this.regexp);

        if (match) {
            var barcode = match[1];

            // Send the target in case there are several barcode widgets on the same page (e.g.
            // registering the lot numbers in a stock picking)
            core.bus.trigger('barcode_scanned', barcode, this.buffered_key_events[0].target);

            // Dispatch a barcode_scanned DOM event to elements that have barcode_events="true" set.
            if (this.buffered_key_events[0].target.getAttribute("barcode_events") === "true")
                $(this.buffered_key_events[0].target).trigger('barcode_scanned', barcode);
        } else {
            this.resend_buffered_keys();
        }

        this.buffered_key_events = [];
    },

    resend_buffered_keys: function() {
        var old_event, new_event;
        for(var i = 0; i < this.buffered_key_events.length; i++) {
            old_event = this.buffered_key_events[i];

            if(old_event.which !== 13) { // ignore returns
                // We do not create a 'real' keypress event through
                // eg. KeyboardEvent because there are several issues
                // with them that make them very different from
                // genuine keypresses. Chrome per example has had a
                // bug for the longest time that causes keyCode and
                // charCode to not be set for events created this way:
                // https://bugs.webkit.org/show_bug.cgi?id=16735
                var params = {
                    'bubbles': old_event.bubbles,
                    'cancelable': old_event.cancelable,
                };
                new_event = $.Event('keypress', params);
                new_event.viewArg = old_event.viewArg;
                new_event.ctrl = old_event.ctrl;
                new_event.alt = old_event.alt;
                new_event.shift = old_event.shift;
                new_event.meta = old_event.meta;
                new_event.char = old_event.char;
                new_event.key = old_event.key;
                new_event.charCode = old_event.charCode;
                new_event.keyCode = old_event.keyCode || old_event.which; // Firefox doesn't set keyCode for keypresses, only keyup/down
                new_event.which = old_event.which;
                new_event.dispatched_by_barcode_reader = true;

                $(old_event.target).trigger(new_event);
            }
        }
    },

    element_is_editable: function(element) {
        return $(element).is('input,textarea,[contenteditable="true"]');
    },

    // This checks that a keypress event is either ESC, TAB, an arrow
    // key or a function key. This is Firefox specific, in Chrom{e,ium}
    // keypress events are not fired for these types of keys, only
    // keyup/keydown.
    is_special_key: function(e) {
        if (e.key === "ArrowLeft" || e.key === "ArrowRight" ||
            e.key === "ArrowUp" || e.key === "ArrowDown" ||
            e.key === "Escape" || e.key === "Tab" ||
            e.key === "Backspace" || e.key === "Delete" ||
            /F\d\d?/.test(e.key)) {
            return true;
        } else {
            return false;
        }
    },

    // The keydown and keyup handlers are here to disallow key
    // repeat. When preventDefault() is called on a keydown event
    // the keypress that normally follows is cancelled.
    keydown_handler: function(e){
        if (this.key_pressed[e.which]) {
            e.preventDefault();
        } else {
            this.key_pressed[e.which] = true;
        }
    },

    keyup_handler: function(e){
        this.key_pressed[e.which] = false;
    },

    handler: function(e){
        // Don't catch events we resent
        if (e.dispatched_by_barcode_reader)
            return;
        // Don't catch non-printable keys for which Firefox triggers a keypress
        if (this.is_special_key(e))
            return;
        // Don't catch keypresses which could have a UX purpose (like shortcuts)
        if (e.ctrlKey || e.metaKey || e.altKey)
            return;
        // Don't catch Return when nothing is buffered. This way users
        // can still use Return to 'click' on focused buttons or links.
        if (e.which === 13 && this.buffered_key_events.length === 0)
            return;
        // Don't catch events targeting elements that are editable because we
        // have no way of redispatching 'genuine' key events. Resent events
        // don't trigger native event handlers of elements. So this means that
        // our fake events will not appear in eg. an <input> element.
        if ((this.element_is_editable(e.target) && !$(e.target).data('enableBarcode')) && e.target.getAttribute("barcode_events") !== "true")
            return;

        // Catch and buffer the event
        this.buffered_key_events.push(e);
        e.preventDefault();
        e.stopImmediatePropagation();

        // Handle buffered keys immediately if the the keypress marks the end
        // of a barcode or after x milliseconds without a new keypress
        clearTimeout(this.timeout);
        if (String.fromCharCode(e.which).match(this.suffix)) {
            this.handle_buffered_keys();
        } else {
            this.timeout = setTimeout(_.bind(this.handle_buffered_keys, this), this.max_time_between_keys_in_ms);
        }
    },

    start: function(prevent_key_repeat){
        $('body').bind("keypress", this.__handler);
        if (prevent_key_repeat === true) {
            $('body').bind("keydown", this.__keydown_handler);
            $('body').bind('keyup', this.__keyup_handler);
        }
    },

    stop: function(){
        $('body').unbind("keypress", this.__handler);
        $('body').unbind("keydown", this.__keydown_handler);
        $('body').unbind('keyup', this.__keyup_handler);
    },
});

return {
    /** Singleton that emits barcode_scanned events on core.bus */
    BarcodeEvents: new BarcodeEvents(),
    /**
     * List of barcode prefixes that are reserved for internal purposes
     * @type Array
     */
    ReservedBarcodePrefixes: ['O-CMD'],
};

});
Beispiel #25
0
odoo.define('web.ActionManager', function (require) {
"use strict";

var ControlPanel = require('web.ControlPanel');
var core = require('web.core');
var crash_manager = require('web.crash_manager');
var data = require('web.data');
var Dialog = require('web.Dialog');
var framework = require('web.framework');
var pyeval = require('web.pyeval');
var session = require('web.session');
var ViewManager = require('web.ViewManager');
var Widget = require('web.Widget');

/**
 * Class representing the actions of the ActionManager
 * Basic implementation for client actions that are functions
 */
var Action = core.Class.extend({
    init: function(action) {
        this.action_descr = action;
        this.title = action.display_name || action.name;
    },
    /**
     * Not implemented for client actions
     * @return {Deferred} a rejected Deferred
     */
    appendTo: function() {
        return $.Deferred().reject();
    },
    /**
     * This method should restore this previously loaded action
     * Calls on_reverse_breadcrumb callback if defined
     * @return {Deferred} resolved when widget is enabled
     */
    restore: function() {
        if (this.on_reverse_breadcrumb) {
            return this.on_reverse_breadcrumb();
        }
    },
    /**
     * Not implemented for functions
     */
    detach: function() {
    },
    /**
     * Destroyer: there is nothing to destroy in the case of a client function
     */
    destroy: function() {
    },
    /**
     * Sets the on_reverse_breadcrumb callback to be called when coming back to that action
     * @param {Function} [on_reverse_breadcrumb] the callback
     */
    set_on_reverse_breadcrumb: function(on_reverse_breadcrumb) {
        this.on_reverse_breadcrumb = on_reverse_breadcrumb;
    },
    /**
     * Stores the DOM fragment of the action
     * @param {jQuery} [fragment] the DOM fragment
     */
    set_fragment: function($fragment) {
        this.$fragment = $fragment;
    },
    /**
     * Not implemented for client actions
     */
    set_is_in_DOM: function() {
    },
    /**
     * @return {Object} the description of the action
     */
    get_action_descr: function() {
        return this.action_descr;
    },
    /**
     * @return {Object} dictionnary that will be interpreted to display the breadcrumbs
     */
    get_breadcrumbs: function() {
        return { title: this.title, action: this };
    },
    /**
     * @return {int} the number of views stacked, i.e. 0 for client functions
     */
    get_nb_views: function() {
        return 0;
    },
    /**
     * @return {jQuery} the DOM fragment of the action
     */
    get_fragment: function() {
        return this.$fragment;
    },
});
/**
 * Specialization of Action for client actions that are Widgets
 */
var WidgetAction = Action.extend({
    /**
     * Initializes the WidgetAction
     * Sets the title of the widget
     */
    init: function(action, widget) {
        this._super(action);

        this.widget = widget;
        if (!this.widget.get('title')) {
            this.widget.set('title', this.title);
        }
        this.widget.on('change:title', this, function(widget) {
            this.title = widget.get('title');
        });
    },
    /**
     * Wraps the action's widget in a container and appends it to el
     * @param {DocumentFragment} [el] where to append the widget
     * @return {Deferred} resolved when the widget is appended
     */
    appendTo: function(el) {
        this.$client_action_container = $('<div>').addClass('oe_client_action');
        this.$client_action_container.appendTo(el);
        return this.widget.appendTo(this.$client_action_container);
    },
    /**
     * Restores WidgetAction by calling do_show on its widget
     */
    restore: function() {
        var self = this;
        return $.when(this._super()).then(function() {
            return self.widget.do_show();
        });
    },
    /**
     * Detaches the client action's container from the DOM
     * @return {jQuery} the action's container
     */
    detach: function() {
        return this.$client_action_container.detach();
    },
    /**
     * Destroys the widget
     */
    destroy: function() {
        this.widget.destroy();
        this.detach();
    },
});
/**
 * Specialization of WidgetAction for window actions (i.e. ViewManagers)
 */
var ViewManagerAction = WidgetAction.extend({
    /**
     * Appends the action's widget to el
     * @param {DocumentFragment} [el] where to append the widget
     * @return {Deferred} resolved when the widget is appended
     */
    appendTo: function(el) {
        return this.widget.appendTo(el);
    },
    /**
     * Restores a ViewManagerAction
     * Switches to the requested view by calling select_view on the ViewManager
     * @param {int} [view_index] the index of the view to select
     */
    restore: function(view_index) {
        var _super = this._super.bind(this);
        return this.widget.select_view(view_index).then(function() {
            return _super();
        });
    },
    /**
     * Sets is_in_DOM on this.widget
     * @param {Boolean} [is_in_DOM] true iff the widget is attached in the DOM
     */
    set_is_in_DOM: function(is_in_DOM) {
        this.widget.is_in_DOM = is_in_DOM;
    },
    /**
     * Detaches the view_manager from the DOM
     * @return {jQuery} the view_manager's $el
     */
    detach: function() {
        return this.widget.$el.detach();
    },
    /**
     * Destroys the widget
     */
    destroy: function() { 
        this.widget.destroy();
    },
    /**
     * @return {Array} array of Objects that will be interpreted to display the breadcrumbs
     */
    get_breadcrumbs: function() {
        var self = this;
        return this.widget.view_stack.map(function (view, index) {
            return {
                title: view.controller.get('title') || self.title,
                index: index,
                action: self,
            };
        });
    },
    /**
     * @return {int} the number of views stacked in the ViewManager
     */
    get_nb_views: function() {
        return this.widget.view_stack.length;
    },
});

var ActionManager = Widget.extend({
    template: "ActionManager",
    init: function(parent) {
        this._super(parent);
        this.action_stack = [];
        this.inner_action = null;
        this.inner_widget = null;
        this.webclient = parent;
        this.dialog = null;
        this.dialog_widget = null;
        this.on('history_back', this, this.proxy('history_back'));
    },
    start: function() {
        this._super();

        // Instantiate a unique main ControlPanel used by widgets of actions in this.action_stack
        this.main_control_panel = new ControlPanel(this);
        // Listen to event "on_breadcrumb_click" trigerred on the control panel when
        // clicking on a part of the breadcrumbs. Call select_action for this breadcrumb.
        this.main_control_panel.on("on_breadcrumb_click", this, function(action, index) {
            this.select_action(action, index);
        });

        // Append the main control panel to the DOM (inside the ActionManager jQuery element)
        this.main_control_panel.appendTo(this.$el);
    },
    dialog_stop: function (reason) {
        if (this.dialog) {
            this.dialog.destroy(reason);
        }
        this.dialog = null;
    },
    /**
     * Add a new action to the action manager
     *
     * widget: typically, widgets added are openerp.web.ViewManager. The action manager
     *      uses the stack of actions to handle the breadcrumbs.
     * action_descr: new action description
     * options.on_reverse_breadcrumb: will be called when breadcrumb is clicked on
     * options.clear_breadcrumbs: boolean, if true, action stack is destroyed
     */
    push_action: function(widget, action_descr, options) {
        var self = this;
        var old_widget = this.inner_widget;
        var old_action = this.inner_action;
        var old_action_stack = this.action_stack;
        options = options || {};

        // Empty action_stack if requested
        if (options.clear_breadcrumbs) {
            this.action_stack = [];
        }

        // Instantiate the new action
        var new_action;
        if (widget instanceof ViewManager) {
            new_action = new ViewManagerAction(action_descr, widget);
        } else if (widget instanceof Widget) {
            new_action = new WidgetAction(action_descr, widget);
        } else {
            new_action = new Action(action_descr);
        }

        // Set on_reverse_breadcrumb callback on previous inner_action
        if (old_action) {
            old_action.set_on_reverse_breadcrumb(options.on_reverse_breadcrumb);
        }

        // Update action_stack (must be done before appendTo to properly
        // compute the breadcrumbs and to perform do_push_state)
        this.action_stack.push(new_action);
        this.inner_action = new_action;
        this.inner_widget = widget;

        if (widget.need_control_panel) {
            // Set the ControlPanel bus on the widget to allow it to communicate its status
            widget.set_cp_bus(this.main_control_panel.get_bus());
        }

        // render the inner_widget in a fragment, and append it to the
        // document only when it's ready
        var new_widget_fragment = document.createDocumentFragment();
        return $.when(this.inner_action.appendTo(new_widget_fragment)).done(function() {
            // Detach the fragment of the previous action and store it within the action
            if (old_action) {
                old_action.set_fragment(old_action.detach());
                old_action.set_is_in_DOM(false);
            }
            if (!widget.need_control_panel) {
                // Hide the main ControlPanel for widgets that do not use it
                self.main_control_panel.do_hide();
            }

            framework.append(self.$el, new_widget_fragment, true);
            self.inner_action.set_is_in_DOM(true);

            // Hide the old_widget as it will be removed from the DOM when it
            // is destroyed
            if (old_widget) {
                old_widget.do_hide();
            }
            if (options.clear_breadcrumbs) {
                self.clear_action_stack(old_action_stack);
            }
        }).fail(function () {
            // Destroy failed action and restore internal state
            new_action.destroy();
            self.action_stack = old_action_stack;
            self.inner_action = old_action;
            self.inner_widget = old_widget;
        });
    },
    get_breadcrumbs: function () {
        return _.flatten(_.map(this.action_stack, function (action) {
            return action.get_breadcrumbs();
        }), true);
    },
    get_title: function () {
        if (this.action_stack.length === 1) {
            // horrible hack to display the action title instead of "New" for the actions
            // that use a form view to edit something that do not correspond to a real model
            // for example, point of sale "Your Session" or most settings form,
            var action = this.action_stack[0];
            if (action.get_breadcrumbs().length === 1) {
                return action.title;
            }
        }
        return _.pluck(this.get_breadcrumbs(), 'title').join(' / ');
    },
    get_action_stack: function () {
        return this.action_stack;
    },
    get_inner_action: function() {
        return this.inner_action;
    },
    get_inner_widget: function() {
        return this.inner_widget;
    },
    history_back: function() {
        var nb_views = this.inner_action.get_nb_views();
        if (nb_views > 1) {
            // Stay on this action, but select the previous view
            return this.select_action(this.inner_action, nb_views - 2);
        }
        if (this.action_stack.length > 1) {
            // Select the previous action
            var action = this.action_stack[this.action_stack.length - 2];
            nb_views = action.get_nb_views();
            return this.select_action(action, nb_views - 1);
        }
        return $.Deferred().reject();
    },
    select_action: function(action, index) {
        var self = this;
        return this.webclient.clear_uncommitted_changes().then(function() {
            // Set the new inner_action/widget and update the action stack
            var old_action = self.inner_action;
            var action_index = self.action_stack.indexOf(action);
            var to_destroy = self.action_stack.splice(action_index + 1);
            self.inner_action = action;
            self.inner_widget = action.widget;

            // Hide the ControlPanel if the widget doesn't use it
            if (!self.inner_widget.need_control_panel) {
                self.main_control_panel.do_hide();
            }

            return $.when(action.restore(index)).done(function() {
                if (action !== old_action) {
                    // Clear the action stack (this also removes the current action from the DOM)
                    self.clear_action_stack(to_destroy);

                    // Append the fragment of the action to restore to self.$el
                    framework.append(self.$el, action.get_fragment(), true);
                    self.inner_action.set_is_in_DOM(true);
                }
            });
        }).fail(function() {
            return $.Deferred().reject();
        });
    },
    clear_action_stack: function(action_stack) {
        _.map(action_stack || this.action_stack, function(action) {
            action.destroy();
        });
        if (!action_stack) {
            this.action_stack = [];
            this.inner_action = null;
            this.inner_widget = null;
        }
    },
    do_push_state: function(state) {
        if (!this.webclient || !this.webclient.do_push_state || this.dialog) {
            return;
        }
        state = state || {};
        if (this.inner_action) {
            var inner_action_descr = this.inner_action.get_action_descr();
            if (inner_action_descr._push_me === false) {
                // this action has been explicitly marked as not pushable
                return;
            }
            state.title = this.get_title();
            if(inner_action_descr.type == 'ir.actions.act_window') {
                state.model = inner_action_descr.res_model;
            }
            if (inner_action_descr.menu_id) {
                state.menu_id = inner_action_descr.menu_id;
            }
            if (inner_action_descr.id) {
                state.action = inner_action_descr.id;
            } else if (inner_action_descr.type == 'ir.actions.client') {
                state.action = inner_action_descr.tag;
                var params = {};
                _.each(inner_action_descr.params, function(v, k) {
                    if(_.isString(v) || _.isNumber(v)) {
                        params[k] = v;
                    }
                });
                state = _.extend(params || {}, state);
            }
            if (inner_action_descr.context) {
                var active_id = inner_action_descr.context.active_id;
                if (active_id) {
                    state.active_id = active_id;
                }
                var active_ids = inner_action_descr.context.active_ids;
                if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) {
                    // We don't push active_ids if it's a single element array containing the active_id
                    // This makes the url shorter in most cases.
                    state.active_ids = inner_action_descr.context.active_ids.join(',');
                }
            }
        }
        this.webclient.do_push_state(state);
    },
    do_load_state: function(state, warm) {
        var self = this;
        var action_loaded;
        if (state.action) {
            if (_.isString(state.action) && core.action_registry.contains(state.action)) {
                var action_client = {
                    type: "ir.actions.client",
                    tag: state.action,
                    params: state,
                    _push_me: state._push_me,
                };
                if (warm) {
                    this.null_action();
                }
                action_loaded = this.do_action(action_client);
            } else {
                var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action;
                if (run_action) {
                    var add_context = {};
                    if (state.active_id) {
                        add_context.active_id = state.active_id;
                    }
                    if (state.active_ids) {
                        // The jQuery BBQ plugin does some parsing on values that are valid integers.
                        // It means that if there's only one item, it will do parseInt() on it,
                        // otherwise it will keep the comma seperated list as string.
                        add_context.active_ids = state.active_ids.toString().split(',').map(function(id) {
                            return parseInt(id, 10) || id;
                        });
                    } else if (state.active_id) {
                        add_context.active_ids = [state.active_id];
                    }
                    add_context.params = state;
                    if (warm) {
                        this.null_action();
                    }
                    action_loaded = this.do_action(state.action, { additional_context: add_context, state: state });
                    $.when(action_loaded || null).done(function() {
                        self.webclient.menu.is_bound.done(function() {
                            if (self.inner_action && self.inner_action.get_action_descr().id) {
                                self.webclient.menu.open_action(self.inner_action.get_action_descr().id);
                            }
                        });
                    });
                }
            }
        } else if (state.model && state.id) {
            // TODO handle context & domain ?
            if (warm) {
                this.null_action();
            }
            var action = {
                res_model: state.model,
                res_id: state.id,
                type: 'ir.actions.act_window',
                views: [[false, 'form']]
            };
            action_loaded = this.do_action(action);
        } else if (state.sa) {
            // load session action
            if (warm) {
                this.null_action();
            }
            action_loaded = this.rpc('/web/session/get_session_action',  {key: state.sa}).then(function(action) {
                if (action) {
                    return self.do_action(action);
                }
            });
        }

        $.when(action_loaded || null).done(function() {
            if (self.inner_widget && self.inner_widget.do_load_state) {
                self.inner_widget.do_load_state(state, warm);
            }
        });
    },
    /**
     * Execute an OpenERP action
     *
     * @param {Number|String|Object} Can be either an action id, a client action or an action descriptor.
     * @param {Object} [options]
     * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list
     * @param {Boolean} [options.replace_breadcrumb=false] Replace the current breadcrumb with the action
     * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on.
     * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb
     * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions)
     * @param {Function} [options.action_menu_id] Manually set the menu id on the fly.
     * @param {Object} [options.additional_context] Additional context to be merged with the action's context.
     * @return {jQuery.Deferred} Action loaded
     */
    do_action: function(action, options) {
        options = _.defaults(options || {}, {
            clear_breadcrumbs: false,
            on_reverse_breadcrumb: function() {},
            hide_breadcrumb: false,
            on_close: function() {},
            action_menu_id: null,
            additional_context: {},
        });
        if (action === false) {
            action = { type: 'ir.actions.act_window_close' };
        } else if (_.isString(action) && core.action_registry.contains(action)) {
            var action_client = { type: "ir.actions.client", tag: action, params: {} };
            return this.do_action(action_client, options);
        } else if (_.isNumber(action) || _.isString(action)) {
            var self = this;
            var additional_context = {
                active_id : options.additional_context.active_id,
                active_ids : options.additional_context.active_ids,
                active_model : options.additional_context.active_model
            };
            return self.rpc("/web/action/load", { action_id: action, additional_context : additional_context }).then(function(result) {
                return self.do_action(result, options);
            });
        }

        core.bus.trigger('action', action);

        // Ensure context & domain are evaluated and can be manipulated/used
        var ncontext = new data.CompoundContext(options.additional_context, action.context || {});
        action.context = pyeval.eval('context', ncontext);
        if (action.context.active_id || action.context.active_ids) {
            // Here we assume that when an `active_id` or `active_ids` is used
            // in the context, we are in a `related` action, so we disable the
            // searchview's default custom filters.
            action.context.search_disable_custom_filters = true;
        }
        if (action.domain) {
            action.domain = pyeval.eval(
                'domain', action.domain, action.context || {});
        }

        if (!action.type) {
            console.error("No type for action", action);
            return $.Deferred().reject();
        }

        var type = action.type.replace(/\./g,'_');
        action.menu_id = options.action_menu_id;
        action.context.params = _.extend({ 'action' : action.id }, action.context.params);
        if (!(type in this)) {
            console.error("Action manager can't handle action of type " + action.type, action);
            return $.Deferred().reject();
        }

        // Special case for Dashboards, this should definitively be done upstream
        if (action.res_model === 'board.board' && action.view_mode === 'form') {
            action.target = 'inline';
            _.extend(action.flags, {
                headless: true,
                views_switcher: false,
                display_title: false,
                search_view: false,
                pager: false,
                sidebar: false,
                action_buttons: false
            });
        } else {
            var popup = action.target === 'new';
            var inline = action.target === 'inline' || action.target === 'inlineview';
            var form = _.str.startsWith(action.view_mode, 'form');
            action.flags = _.defaults(action.flags || {}, {
                views_switcher : !popup && !inline,
                search_view : !popup && !inline,
                action_buttons : !popup && !inline,
                sidebar : !popup && !inline,
                pager : (!popup || !form) && !inline,
                display_title : !popup,
                headless: (popup || inline) && form,
                search_disable_custom_filters: action.context && action.context.search_disable_custom_filters,
            });
        }

        return this[type](action, options);
    },
    null_action: function() {
        this.dialog_stop();
        this.clear_action_stack();
    },
    /**
     *
     * @param {Object} executor
     * @param {Object} executor.action original action
     * @param {Function<instance.web.Widget>} executor.widget function used to fetch the widget instance
     * @param {String} executor.klass CSS class to add on the dialog root, if action.target=new
     * @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget
     * @param {Object} options
     * @return {*}
     */
    ir_actions_common: function(executor, options) {
        if (executor.action.target === 'new') {
            var pre_dialog = (this.dialog && !this.dialog.isDestroyed()) ? this.dialog : null;
            if (pre_dialog){
                // prevent previous dialog to consider itself closed,
                // right now, as we're opening a new one (prevents
                // reload of original form view)
                pre_dialog.off('closed', null, pre_dialog.on_close);
            }
            if (this.dialog_widget && !this.dialog_widget.isDestroyed()) {
                this.dialog_widget.destroy();
            }
            // explicitly passing a closing action to dialog_stop() prevents
            // it from reloading the original form view
            this.dialog_stop(executor.action);
            this.dialog = new Dialog(this, {
                title: executor.action.name,
                dialogClass: executor.klass,
                buttons: []
            });

            // chain on_close triggers with previous dialog, if any
            this.dialog.on_close = function(){
                options.on_close.apply(null, arguments);
                if (pre_dialog && pre_dialog.on_close){
                    // no parameter passed to on_close as this will
                    // only be called when the last dialog is truly
                    // closing, and *should* trigger a reload of the
                    // underlying form view (see comments above)
                    pre_dialog.on_close();
                }
            };
            this.dialog.on("closed", null, this.dialog.on_close);
            this.dialog_widget = executor.widget();
            if (this.dialog_widget instanceof ViewManager) {
                _.extend(this.dialog_widget.flags, {
                    $buttons: this.dialog.$footer,
                    footer_to_buttons: true,
                });
                if (this.dialog_widget.action.view_mode === 'form') {
                    this.dialog_widget.flags.headless = true;
                }
            }
            if (this.dialog_widget.need_control_panel) {
                // Set a fake bus to Dialogs needing a ControlPanel as they should not
                // communicate with the main ControlPanel
                this.dialog_widget.set_cp_bus(new core.Bus());
            }
            this.dialog_widget.setParent(this.dialog);
            this.dialog.open();
            
            return this.dialog_widget.appendTo(this.dialog.$el);
        }
        // Clear uncommitted changes on the current inner widget if there is one
        var self = this;
        var def = (this.inner_widget && this.webclient.clear_uncommitted_changes()) || $.when();
        return def.then(function() {
            self.dialog_stop(executor.action);
            return self.push_action(executor.widget(), executor.action, options);
        }).fail(function() {
            return $.Deferred().reject();
        });
    },
    ir_actions_act_window: function (action, options) {
        var self = this;
        return this.ir_actions_common({
            widget: function () {
                return new ViewManager(self, null, null, null, action, options);
            },
            action: action,
            klass: 'o_act_window',
        }, options);
    },
    ir_actions_client: function (action, options) {
        var self = this;
        var ClientWidget = core.action_registry.get(action.tag);
        if (!ClientWidget) {
            return self.do_warn("Action Error", "Could not find client action '" + action.tag + "'.");
        }
        if (!(ClientWidget.prototype instanceof Widget)) {
            var next;
            if ((next = ClientWidget(this, action))) {
                return this.do_action(next, options);
            }
            return $.when();
        }

        return this.ir_actions_common({
            widget: function () {
                return new ClientWidget(self, action);
            },
            action: action,
            klass: 'oe_act_client',
        }, options).then(function () {
            if (action.tag !== 'reload') {self.do_push_state({});}
        });
    },
    ir_actions_act_window_close: function (action, options) {
        if (!this.dialog) {
            options.on_close();
        }
        this.dialog_stop();
        return $.when();
    },
    ir_actions_server: function (action, options) {
        var self = this;
        this.rpc('/web/action/run', {
            action_id: action.id,
            context: action.context || {}
        }).done(function (action) {
            self.do_action(action, options);
        });
    },
    ir_actions_report_xml: function(action, options) {
        var self = this;
        framework.blockUI();
        action = _.clone(action);
        var eval_contexts = ([session.user_context] || []).concat([action.context]);
        action.context = pyeval.eval('contexts',eval_contexts);

        // iOS devices doesn't allow iframe use the way we do it,
        // opening a new window seems the best way to workaround
        if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
            var params = {
                action: JSON.stringify(action),
                token: new Date().getTime()
            };
            var url = self.session.url('/web/report', params);
            framework.unblockUI();
            $('<a href="'+url+'" target="_blank"></a>')[0].click();
            return;
        }
        var c = crash_manager;
        return $.Deferred(function (d) {
            self.session.get_file({
                url: '/web/report',
                data: {action: JSON.stringify(action)},
                complete: framework.unblockUI,
                success: function(){
                    if (!self.dialog) {
                        options.on_close();
                    }
                    self.dialog_stop();
                    d.resolve();
                },
                error: function () {
                    c.rpc_error.apply(c, arguments);
                    d.reject();
                }
            });
        });
    },
    ir_actions_act_url: function (action) {
        if (action.target === 'self') {
            framework.redirect(action.url);
        } else {
            window.open(action.url, '_blank');
        }
        return $.when();
    },
});

return ActionManager;

});
Beispiel #26
0
odoo.define('web.Widget', function (require) {
"use strict";

var ajax = require('web.ajax');
var core = require('web.core');
var mixins = require('web.mixins');
var ServicesMixin = require('web.ServicesMixin');

/**
 * Base class for all visual components. Provides a lot of functions helpful
 * for the management of a part of the DOM.
 *
 * Widget handles:
 *
 * - Rendering with QWeb.
 * - Life-cycle management and parenting (when a parent is destroyed, all its
 *   children are destroyed too).
 * - Insertion in DOM.
 *
 * **Guide to create implementations of the Widget class**
 *
 * Here is a sample child class::
 *
 *     var MyWidget = Widget.extend({
 *         // the name of the QWeb template to use for rendering
 *         template: "MyQWebTemplate",
 *
 *         init: function (parent) {
 *             this._super(parent);
 *             // stuff that you want to init before the rendering
 *         },
 *         willStart: function () {
 *             // async work that need to be done before the widget is ready
 *             // this method should return a deferred
 *         },
 *         start: function() {
 *             // stuff you want to make after the rendering, `this.$el` holds a correct value
 *             this.$(".my_button").click(/* an example of event binding * /);
 *
 *             // if you have some asynchronous operations, it's a good idea to return
 *             // a promise in start(). Note that this is quite rare, and if you
 *             // need to fetch some data, this should probably be done in the
 *             // willStart method
 *             var promise = this._rpc(...);
 *             return promise;
 *         }
 *     });
 *
 * Now this class can simply be used with the following syntax::
 *
 *     var myWidget = new MyWidget(this);
 *     myWidget.appendTo($(".some-div"));
 *
 * With these two lines, the MyWidget instance was initialized, rendered,
 * inserted into the DOM inside the ``.some-div`` div and its events were
 * bound.
 *
 * And of course, when you don't need that widget anymore, just do::
 *
 *     myWidget.destroy();
 *
 * That will kill the widget in a clean way and erase its content from the dom.
 */

var Widget = core.Class.extend(mixins.PropertiesMixin, ServicesMixin, {
    // Backbone-ish API
    tagName: 'div',
    id: null,
    className: null,
    attributes: {},
    events: {},
    /**
     * The name of the QWeb template that will be used for rendering. Must be
     * redefined in subclasses or the default render() method can not be used.
     *
     * @type {null|string}
     */
    template: null,
    /**
     * List of paths to xml files that need to be loaded before the widget can
     * be rendered. This will not induce loading anything that has already been
     * loaded.
     *
     * @type {null|string[]}
     */
    xmlDependencies: null,

    /**
     * Constructs the widget and sets its parent if a parent is given.
     *
     * @param {Widget|null} parent Binds the current instance to the given Widget
     *   instance. When that widget is destroyed by calling destroy(), the
     *   current instance will be destroyed too. Can be null.
     */
    init: function (parent) {
        mixins.PropertiesMixin.init.call(this);
        this.setParent(parent);
        // Bind on_/do_* methods to this
        // We might remove this automatic binding in the future
        for (var name in this) {
            if(typeof(this[name]) === "function") {
                if((/^on_|^do_/).test(name)) {
                    this[name] = this[name].bind(this);
                }
            }
        }
    },
    /**
     * Method called between @see init and @see start. Performs asynchronous
     * calls required by the rendering and the start method.
     *
     * This method should return a Deferred which is resolved when start can be
     * executed.
     *
     * @returns {Deferred}
     */
    willStart: function () {
        if (this.xmlDependencies) {
            var defs = _.map(this.xmlDependencies, function (xmlPath) {
                return ajax.loadXML(xmlPath, core.qweb);
            });
            return $.when.apply($, defs);
        }
        return $.when();
    },
    /**
     * Method called after rendering. Mostly used to bind actions, perform
     * asynchronous calls, etc...
     *
     * By convention, this method should return an object that can be passed to
     * $.when() to inform the caller when this widget has been initialized.
     *
     * Note that, for historic reasons, many widgets still do work in the start
     * method that would be more suited to the willStart method.
     *
     * @returns {Deferred}
     */
    start: function () {
        return $.when();
    },
    /**
     * Destroys the current widget, also destroys all its children before
     * destroying itself.
     */
    destroy: function () {
        _.invoke(this.getChildren(), 'destroy');
        if(this.$el) {
            this.$el.remove();
        }
        mixins.PropertiesMixin.destroy.call(this);
    },

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

    /**
     * Renders the current widget and appends it to the given jQuery object.
     *
     * @param {jQuery} target
     */
    appendTo: function (target) {
        var self = this;
        return this._widgetRenderAndInsert(function (t) {
            self.$el.appendTo(t);
        }, target);
    },
    /**
     * Attach the current widget to a dom element
     *
     * @param {jQuery} target
     */
    attachTo: function (target) {
        var self = this;
        this.setElement(target.$el || target);
        return this.willStart().then(function () {
            return self.start();
        });
    },
    /**
     * Hides the widget
     */
    do_hide: function () {
        this.$el.addClass('o_hidden');
    },
    /**
     * Displays the widget
     */
    do_show: function () {
        this.$el.removeClass('o_hidden');
    },
    /**
     * Displays or hides the widget
     * @param {boolean} [display] use true to show the widget or false to hide it
     */
    do_toggle: function (display) {
        if (_.isBoolean(display)) {
            display ? this.do_show() : this.do_hide();
        } else {
            this.$el.hasClass('o_hidden') ? this.do_show() : this.do_hide();
        }
    },
    /**
     * Renders the current widget and inserts it after to the given jQuery
     * object.
     *
     * @param {jQuery} target
     */
    insertAfter: function (target) {
        var self = this;
        return this._widgetRenderAndInsert(function (t) {
            self.$el.insertAfter(t);
        }, target);
    },
    /**
     * Renders the current widget and inserts it before to the given jQuery
     * object.
     *
     * @param {jQuery} target
     */
    insertBefore: function (target) {
        var self = this;
        return this._widgetRenderAndInsert(function (t) {
            self.$el.insertBefore(t);
        }, target);
    },
    /**
     * Renders the current widget and prepends it to the given jQuery object.
     *
     * @param {jQuery} target
     */
    prependTo: function (target) {
        var self = this;
        return this._widgetRenderAndInsert(function (t) {
            self.$el.prependTo(t);
        }, target);
    },
    /**
     * Renders the element. The default implementation renders the widget using
     * QWeb, `this.template` must be defined. The context given to QWeb contains
     * the "widget" key that references `this`.
     */
    renderElement: function () {
        var $el;
        if (this.template) {
            $el = $(core.qweb.render(this.template, {widget: this}).trim());
        } else {
            $el = this._makeDescriptive();
        }
        this._replaceElement($el);
    },
    /**
     * Renders the current widget and replaces the given jQuery object.
     *
     * @param target A jQuery object or a Widget instance.
     */
    replace: function (target) {
        return this._widgetRenderAndInsert(_.bind(function (t) {
            this.$el.replaceAll(t);
        }, this), target);
    },
    /**
     * Re-sets the widget's root element (el/$el/$el).
     *
     * Includes:
     *
     * * re-delegating events
     * * re-binding sub-elements
     * * if the widget already had a root element, replacing the pre-existing
     *   element in the DOM
     *
     * @param {HTMLElement | jQuery} element new root element for the widget
     * @return {Widget} this
     */
    setElement: function (element) {
        if (this.$el) {
            this._undelegateEvents();
        }

        this.$el = (element instanceof $) ? element : $(element);
        this.el = this.$el[0];

        this._delegateEvents();

        return this;
    },

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

    /**
     * Helper method, for ``this.$el.find(selector)``
     *
     * @private
     * @param {string} selector CSS selector, rooted in $el
     * @returns {jQuery} selector match
     */
    $: function (selector) {
        if (selector === undefined) {
            return this.$el;
        }
        return this.$el.find(selector);
    },
    /**
     * Attach event handlers for events described in the 'events' key
     *
     * @private
     */
    _delegateEvents: function () {
        var events = this.events;
        if (_.isEmpty(events)) { return; }

        for(var key in events) {
            if (!events.hasOwnProperty(key)) { continue; }

            var method = this.proxy(events[key]);

            var match = /^(\S+)(\s+(.*))?$/.exec(key);
            var event = match[1];
            var selector = match[3];

            event += '.widget_events';
            if (!selector) {
                this.$el.on(event, method);
            } else {
                this.$el.on(event, selector, method);
            }
        }
    },
    /**
     * Makes a potential root element from the declarative builder of the
     * widget
     *
     * @private
     * @return {jQuery}
     */
    _makeDescriptive: function () {
        var attrs = _.extend({}, this.attributes || {});
        if (this.id) {
            attrs.id = this.id;
        }
        if (this.className) {
            attrs['class'] = this.className;
        }
        var $el = $(document.createElement(this.tagName));
        if (!_.isEmpty(attrs)) {
            $el.attr(attrs);
        }
        return $el;
    },
    /**
     * Re-sets the widget's root element and replaces the old root element
     * (if any) by the new one in the DOM.
     *
     * @private
     * @param {HTMLElement | jQuery} $el
     * @returns {Widget} this instance, so it can be chained
     */
    _replaceElement: function ($el) {
        var $oldel = this.$el;
        this.setElement($el);
        if ($oldel && !$oldel.is(this.$el)) {
            if ($oldel.length > 1) {
                $oldel.wrapAll('<div/>');
                $oldel.parent().replaceWith(this.$el);
            } else {
                $oldel.replaceWith(this.$el);
            }
        }
        return this;
    },
    /**
     * Remove all handlers registered on this.$el
     *
     * @private
     */
    _undelegateEvents: function () {
        this.$el.off('.widget_events');
    },
    /**
     * Render the widget.  This is a private method, and should really never be
     * called by anyone (except this widget).  It assumes that the widget was
     * not willStarted yet.
     *
     * @private
     * @param {function: jQuery -> any} insertion
     * @param {jQuery} target
     * @returns {Deferred}
     */
    _widgetRenderAndInsert: function (insertion, target) {
        var self = this;
        return this.willStart().then(function () {
            self.renderElement();
            insertion(target);
            return self.start();
        });
    },
});

return Widget;

});
Beispiel #27
0
odoo.define('web.DataManager', function (require) {
"use strict";

var config = require('web.config');
var core = require('web.core');
var fieldRegistry = require('web.field_registry');
var pyeval = require('web.pyeval');
var rpc = require('web.rpc');
var utils = require('web.utils');

return core.Class.extend({
    init: function () {
        this._init_cache();
        core.bus.on('clear_cache', this, this.invalidate.bind(this));
    },

    _init_cache: function () {
        this._cache = {
            actions: {},
            fields_views: {},
            filters: {},
            views: {},
        };
    },

    /**
     * Invalidates the whole cache
     * Suggestion: could be refined to invalidate some part of the cache
     */
    invalidate: function () {
        this._init_cache();
    },

    /**
     * Loads an action from its id or xmlid.
     *
     * @param {int|string} [action_id] the action id or xmlid
     * @param {Object} [additional_context] used to load the action
     * @return {Deferred} resolved with the action whose id or xmlid is action_id
     */
    load_action: function (action_id, additional_context) {
        var self = this;
        var key = this._gen_key(action_id, additional_context || {});

        if (!this._cache.actions[key]) {
            this._cache.actions[key] = rpc.query({
                route: "/web/action/load",
                params: {
                    action_id: action_id,
                    additional_context : additional_context,
                },
            }).then(function (action) {
                self._cache.actions[key] = action.no_cache ? null : self._cache.actions[key];
                return action;
            }, this._invalidate.bind(this, this._cache.actions, key));
        }

        return this._cache.actions[key].then(function (action) {
            return $.extend(true, {}, action);
        });
    },

    /**
     * Loads various information concerning views: fields_view for each view,
     * the fields of the corresponding model, and optionally the filters.
     *
     * @param {Object} params
     * @param {String} params.model
     * @param {Object} params.context
     * @param {Array} params.views_descr array of [view_id, view_type]
     * @param {Object} [options] dictionary of various options:
     *     - options.load_filters: whether or not to load the filters,
     *     - options.action_id: the action_id (required to load filters),
     *     - options.toolbar: whether or not a toolbar will be displayed,
     * @return {Deferred} resolved with the requested views information
     */
    load_views: function (params, options) {
        var self = this;

        var model = params.model;
        var context = params.context;
        var views_descr = params.views_descr;
        var key = this._gen_key(model, views_descr, options || {}, context);

        if (!this._cache.views[key]) {
            // Don't load filters if already in cache
            var filters_key;
            if (options.load_filters) {
                filters_key = this._gen_key(model, options.action_id);
                options.load_filters = !this._cache.filters[filters_key];
            }

            this._cache.views[key] = rpc.query({
                args: [],
                kwargs: {
                    views: views_descr,
                    options: options,
                    context: context.eval(),
                },
                model: model,
                method: 'load_views',
            }).then(function (result) {
                // Postprocess fields_views and insert them into the fields_views cache
                result.fields_views = _.mapObject(result.fields_views, self._postprocess_fvg.bind(self));
                self.processViews(result.fields_views, result.fields);
                _.each(views_descr, function (view_descr) {
                    var toolbar = options.toolbar && view_descr[1] !== 'search';
                    var fv_key = self._gen_key(model, view_descr[0], view_descr[1], toolbar, context);
                    self._cache.fields_views[fv_key] = $.when(result.fields_views[view_descr[1]]);
                });

                // Insert filters, if any, into the filters cache
                if (result.filters) {
                    self._cache.filters[filters_key] = $.when(result.filters);
                }

                return result.fields_views;
            }, this._invalidate.bind(this, this._cache.views, key));
        }

        return this._cache.views[key];
    },

    /**
     * Loads the filters of a given model and optional action id.
     *
     * @param {Object} [dataset] the dataset for which the filters are loaded
     * @param {int} [action_id] the id of the action (optional)
     * @return {Deferred} resolved with the requested filters
     */
    load_filters: function (dataset, action_id) {
        var key = this._gen_key(dataset.model, action_id);
        if (!this._cache.filters[key]) {
            this._cache.filters[key] = rpc.query({
                args: [dataset.model, action_id],
                kwargs: {
                    context: dataset.get_context(),
                },
                model: 'ir.filters',
                method: 'get_filters',
            }).fail(this._invalidate.bind(this, this._cache.filters, key));
        }
        return this._cache.filters[key];
    },

    /**
     * Calls 'create_or_replace' on 'ir_filters'.
     *
     * @param {Object} [filter] the filter description
     * @return {Deferred} resolved with the id of the created or replaced filter
     */
    create_filter: function (filter) {
        var self = this;
        return rpc.query({
                args: [filter],
                model: 'ir.filters',
                method: 'create_or_replace',
            })
            .then(function (filter_id) {
                var key = [
                    filter.model_id,
                    filter.action_id || false,
                ].join(',');
                self._invalidate(self._cache.filters, key);
                return filter_id;
            });
    },

    /**
     * Calls 'unlink' on 'ir_filters'.
     *
     * @param {Object} [filter] the description of the filter to remove
     * @return {Deferred}
     */
    delete_filter: function (filter) {
        var self = this;
        return rpc.query({
                args: [filter.id],
                model: 'ir.filters',
                method: 'unlink',
            })
            .then(function () {
                self._cache.filters = {}; // invalidate cache
            });
    },

    /**
     * Processes fields and fields_views. For each field, writes its name inside
     * the field description to make it self-contained. For each fields_view,
     * completes its fields with the missing ones.
     *
     * @param {Object} fieldsViews object of fields_views (keys are view types)
     * @param {Object} fields all the fields of the model
     */
    processViews: function (fieldsViews, fields) {
        var fieldName, fieldsView, viewType;
        // write the field name inside the description for all fields
        for (fieldName in fields) {
            fields[fieldName].name = fieldName;
        }
        for (viewType in fieldsViews) {
            fieldsView = fieldsViews[viewType];
            // write the field name inside the description for fields in view
            for (fieldName in fieldsView.fields) {
                fieldsView.fields[fieldName].name = fieldName;
            }
            // complete fields (in view) with missing ones
            _.defaults(fieldsView.fields, fields);
            // process the fields_view
            _.extend(fieldsView, this._processFieldsView({
                type: viewType,
                arch: fieldsView.arch,
                fields: fieldsView.fields,
            }));
        }
    },

    /**
     * Private function that postprocesses fields_view (mainly parses the arch attribute)
     */
    _postprocess_fvg: function (fields_view) {
        var self = this;

        // Parse arch
        var doc = $.parseXML(fields_view.arch).documentElement;
        fields_view.arch = utils.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));

        // Process inner views (x2manys)
        _.each(fields_view.fields, function(field) {
            _.each(field.views || {}, function(view) {
                self._postprocess_fvg(view);
            });
        });

        return fields_view;
    },

    /**
     * Private function that generates a cache key from its arguments
     */
    _gen_key: function () {
        return _.map(Array.prototype.slice.call(arguments), function (arg) {
            if (!arg) {
                return false;
            }
            return _.isObject(arg) ? JSON.stringify(arg) : arg;
        }).join(',');
    },

    /**
     * Private function that invalidates a cache entry
     */
    _invalidate: function (cache, key) {
        delete cache[key];
    },

    ///////////////////////////////////////////////////////////////

    /**
     * Process a field node, in particular, put a flag on the field to give
     * special directives to the BasicModel.
     *
     * @param {string} viewType
     * @param {Object} field - the field properties
     * @param {Object} attrs - the field attributes (from the xml)
     * @returns {Object} attrs
     */
    _processField: function (viewType, field, attrs) {
        var self = this;
        attrs.Widget = this._getFieldWidgetClass(viewType, field, attrs);

        if (!_.isObject(attrs.options)) { // parent arch could have already been processed (TODO this should not happen)
            attrs.options = attrs.options ? pyeval.py_eval(attrs.options) : {};
        }

        if (attrs.on_change && !field.onChange) {
            field.onChange = "1";
        }

        // the relational data of invisible relational fields should not be
        // fetched (e.g. name_gets of invisible many2ones), at least those that
        // are always invisible.
        // the invisible attribute of a field is supposed to be static ("1" in
        // general), but not totally as it may use keys of the context
        // ("context.get('some_key')"). It is evaluated server-side, and the
        // result is put inside the modifiers as a value of the '(column_)invisible'
        // key, and the raw value is left in the invisible attribute (it is used
        // in debug mode for informational purposes).
        // this should change, for instance the server might set the evaluated
        // value in invisible, which could then be seen as static by the client,
        // and add another key in debug mode containing the raw value.
        // for now, we look inside the modifiers and consider the value only if
        // it is static (=== true),
        if (attrs.modifiers.invisible === true || attrs.modifiers.column_invisible === true) {
            attrs.__no_fetch = true;
        }

        if (!_.isEmpty(field.views)) {
            // process the inner fields_view as well to find the fields they use.
            // register those fields' description directly on the view.
            // for those inner views, the list of all fields isn't necessary, so
            // basically the field_names will be the keys of the fields obj.
            // don't use _ to iterate on fields in case there is a 'length' field,
            // as _ doesn't behave correctly when there is a length key in the object
            attrs.views = {};
            _.each(field.views, function (innerFieldsView, viewType) {
                viewType = viewType === 'tree' ? 'list' : viewType;
                innerFieldsView.type = viewType;
                attrs.views[viewType] = self._processFieldsView(_.extend({}, innerFieldsView));
            });
            delete field.views;
        }

        if (field.type === 'one2many' || field.type === 'many2many') {
            if (attrs.Widget.prototype.useSubview) {
                if (!attrs.views) {
                    attrs.views = {};
                }
                var mode = attrs.mode;
                if (!mode) {
                    if (attrs.views.tree && attrs.views.kanban) {
                        mode = 'tree';
                    } else if (!attrs.views.tree && attrs.views.kanban) {
                        mode = 'kanban';
                    } else {
                        mode = 'tree,kanban';
                    }
                }
                if (mode.indexOf(',') !== -1) {
                    mode = config.device.size_class !== config.device.SIZES.XS ? 'tree' : 'kanban';
                }
                if (mode === 'tree') {
                    mode = 'list';
                    if (!attrs.views.list && attrs.views.tree) {
                        attrs.views.list = attrs.views.tree;
                    }
                }
                attrs.mode = mode;
                if (mode in attrs.views) {
                    var view = attrs.views[mode];
                    var defaultOrder = view.arch.attrs.default_order;
                    if (defaultOrder) {
                        // process the default_order, which is like 'name,id desc'
                        // but we need it like [{name: 'name', asc: true}, {name: 'id', asc: false}]
                        attrs.orderedBy = _.map(defaultOrder.split(','), function (order) {
                            order = order.trim().split(' ');
                            return {name: order[0], asc: order[1] !== 'desc'};
                        });
                    } else {
                        // if there is a field with widget `handle`, the x2many
                        // needs to be ordered by this field to correctly display
                        // the records
                        var handleField = _.find(view.arch.children, function (child) {
                            return child.attrs && child.attrs.widget === 'handle';
                        });
                        if (handleField) {
                            attrs.orderedBy = [{name: handleField.attrs.name, asc: true}];
                        }
                    }

                    attrs.columnInvisibleFields = {};
                    _.each(view.arch.children, function (child) {
                        if (child.attrs && child.attrs.modifiers) {
                            attrs.columnInvisibleFields[child.attrs.name] =
                                child.attrs.modifiers.column_invisible || false;
                        }
                    });

                    // detect editables list has they behave differently with respect
                    // to the sorting (changes are not sorted directly)
                    if (mode === 'list' && view.arch.attrs.editable) {
                         attrs.keepChangesUnsorted = true;
                    }
                }
            }
            if (attrs.Widget.prototype.fieldsToFetch) {
                attrs.viewType = 'default';
                attrs.relatedFields = _.extend({}, attrs.Widget.prototype.fieldsToFetch);
                attrs.fieldsInfo = {
                    default: _.mapObject(attrs.Widget.prototype.fieldsToFetch, function () {
                        return {};
                    }),
                };
                if (attrs.options.color_field) {
                    // used by m2m tags
                    attrs.relatedFields[attrs.options.color_field] = { type: 'integer' };
                    attrs.fieldsInfo.default[attrs.options.color_field] = {};
                }
            }
        }
        return attrs;
    },
    /**
     * Visit all nodes in the arch field and process each fields
     *
     * @param {string} viewType
     * @param {Object} arch
     * @param {Object} fields
     * @returns {Object} fieldsInfo
     */
    _processFields: function (viewType, arch, fields) {
        var self = this;
        var fieldsInfo = Object.create(null);
        utils.traverse(arch, function (node) {
            if (typeof node === 'string') {
                return false;
            }
            if (!_.isObject(node.attrs.modifiers)) {
                node.attrs.modifiers = node.attrs.modifiers ? JSON.parse(node.attrs.modifiers) : {};
            }
            if (node.tag === 'field') {
                fieldsInfo[node.attrs.name] = self._processField(viewType,
                    fields[node.attrs.name], node.attrs ? _.clone(node.attrs) : {});
                return false;
            }
            return node.tag !== 'arch';
        });
        return fieldsInfo;
    },
    /**
     * Visit all nodes in the arch field and process each fields and inner views
     *
     * @param {Object} viewInfo
     * @param {Object} viewInfo.arch
     * @param {Object} viewInfo.fields
     * @returns {Object} viewInfo
     */
    _processFieldsView: function (viewInfo) {
        var viewFields = this._processFields(viewInfo.type, viewInfo.arch, viewInfo.fields);
        viewInfo.fieldsInfo = {};
        viewInfo.fieldsInfo[viewInfo.type] = viewFields;
        utils.deepFreeze(viewInfo.fields);
        return viewInfo;
    },
    /**
     * Returns the AbstractField specialization that should be used for the
     * given field informations. If there is no mentioned specific widget to
     * use, determine one according the field type.
     *
     * @param {string} viewType
     * @param {Object} field
     * @param {Object} attrs
     * @returns {function|null} AbstractField specialization Class
     */
    _getFieldWidgetClass: function (viewType, field, attrs) {
        var Widget;
        if (attrs.widget) {
            Widget = fieldRegistry.getAny([viewType + "." + attrs.widget, attrs.widget]);
            if (!Widget) {
                console.warn("Missing widget: ", attrs.widget, " for field", attrs.name, "of type", field.type);
            }
        } else if (viewType === 'kanban' && field.type === 'many2many') {
            // we want to display the widget many2manytags in kanban even if it
            // is not specified in the view
            Widget = fieldRegistry.get('kanban.many2many_tags');
        }
        return Widget || fieldRegistry.getAny([viewType + "." + field.type, field.type, "abstract"]);
    },
});

});
ecore.define('website_blog.InlineDiscussion', function (require) {
'use strict';

// Inspired from https://github.com/tsi/inlineDisqussions

var ajax = require('web.ajax');
var core = require('web.core');
var base = require('web_editor.base');

var qweb = core.qweb;

ajax.loadXML('/website_blog/static/src/xml/website_blog.inline.discussion.xml', qweb);


var InlineDiscussion = core.Class.extend({
    init: function(options) {
        var defaults = {
            position: 'right',
            post_id: $('#blog_post_name').attr('data-blog-id'),
            content : false,
            public_user: false,
        };
        this.settings = $.extend({}, defaults, options);
    },
    start: function() {
        var self = this;
        if ($('#discussions_wrapper').length === 0 && this.settings.content.length > 0) {
            $('<div id="discussions_wrapper"></div>').insertAfter($('#blog_content'));
        }
        // Attach a discussion to each paragraph.
        this.discussions_handler(this.settings.content);

        // Hide the discussion.
        $('html').click(function(event) {
            if($(event.target).parents('#discussions_wrapper, .main-discussion-link-wrp').length === 0) {
                self.hide_discussion();
            }
            if(!$(event.target).hasClass('discussion-link') && !$(event.target).parents('.popover').length){
                if($('.move_discuss').length){
                    $('[enable_chatter_discuss=True]').removeClass('move_discuss');
                    $('[enable_chatter_discuss=True]').animate({
                        'marginLeft': "+=40%"
                    });
                    $('#discussions_wrapper').animate({
                        'marginLeft': "+=250px"
                    });
                }
            }
        });
    },
    prepare_data : function(identifier, comment_count) {
        var self = this;
        return ajax.jsonRpc("/blog/post_get_discussion/", 'call', {
            'post_id': self.settings.post_id,
            'path': identifier,
            'count': comment_count, //if true only get length of total comment, display on discussion thread.
        });
    },
    prepare_multi_data : function(identifiers, comment_count) {
        var self = this;
        return ajax.jsonRpc("/blog/post_get_discussions/", 'call', {
            'post_id': self.settings.post_id,
            'paths': identifiers,
            'count': comment_count, //if true only get length of total comment, display on discussion thread.
        });
    },
    discussions_handler: function() {
        var self = this;
        var node_by_id = {};
        $(self.settings.content).each(function() {
            var node = $(this);
            var identifier = node.attr('data-chatter-id');
            if (identifier) {
                node_by_id[identifier] = node;
            }
        });
        self.prepare_multi_data(_.keys(node_by_id), true).then( function (multi_data) {
            _.forEach(multi_data, function(data) {
                self.prepare_discuss_link(data.val, data.path, node_by_id[data.path]);
            });
        });
    },
    prepare_discuss_link :  function(data, identifier, node) {
        var self = this;
        var cls = data > 0 ? 'discussion-link has-comments' : 'discussion-link';
        var a = $('<a class="'+ cls +' css_editable_mode_hidden" />')
            .attr('data-discus-identifier', identifier)
            .attr('data-discus-position', self.settings.position)
            .text(data > 0 ? data : '+')
            .attr('data-contentwrapper', '.mycontent')
            .wrap('<div class="discussion" />')
            .parent()
            .appendTo('#discussions_wrapper');
        a.css({
            'top': node.offset().top,
            'left': self.settings.position == 'right' ? node.outerWidth() + node.offset().left: node.offset().left - a.outerWidth()
        });
        // node.attr('data-discus-identifier', identifier)
        node.mouseover(function() {
            a.addClass("hovered");
        }).mouseout(function() {
            a.removeClass("hovered");
        });

        a.delegate('a.discussion-link', "click", function(e) {
            e.preventDefault();
            if(!$('.move_discuss').length){
                $('[enable_chatter_discuss=True]').addClass('move_discuss');
                $('[enable_chatter_discuss=True]').animate({
                    'marginLeft': "-=40%"
                });
                $('#discussions_wrapper').animate({
                    'marginLeft': "-=250px"
                });
            }
            if ($(this).is('.active')) {
                e.stopPropagation();
                self.hide_discussion();
            }
            else {
                self.get_discussion($(this), function() {});
            }
        });
    },
    get_discussion : function(source, callback) {
        var self = this;
        var identifier = source.attr('data-discus-identifier');
        self.hide_discussion();
        self.discus_identifier = identifier;
        var elt = $('a[data-discus-identifier="'+identifier+'"]');
        self.settings.current_url = window.location;
        elt.append(qweb.render("website.blog_discussion.popover", {'identifier': identifier , 'options': self.settings}));
        var comment = '';
        self.prepare_data(identifier,false).then(function(data){
            _.each(data, function(res){
                comment += qweb.render("website.blog_discussion.comment", {'res': res});
            });
            $('.discussion_history').html('<ul class="media-list mt8">' + comment + '</ul>');
            self.create_popover(elt, identifier);
            // Add 'active' class.
            $('a.discussion-link, a.main-discussion-link').removeClass('active').filter(source).addClass('active');
            elt.popover('hide').filter(source).popover('show');
            callback(source);
        });
    },
    create_popover : function(elt, identifier) {
        var self = this;
        elt.popover({
            placement:'right',
            trigger:'manual',
            html:true, content:function(){
                return $($(this).data('contentwrapper')).html();
            }
        }).parent().delegate(self).on('click','button#comment_post',function(e) {
            e.stopImmediatePropagation();
            self.post_discussion(identifier);
        });
    },
    validate : function(public_user){
        var comment = $(".popover textarea#inline_comment").val();
        if(!comment) {
            $('div#inline_comment').addClass('has-error');
            return false;
        }
        $("div#inline_comment").removeClass('has-error');
        $(".popover textarea#inline_comment").val('');
        return [comment];
    },
    post_discussion : function() {
        var self = this;
        var val = self.validate(self.settings.public_user);
        if(!val) return;
        ajax.jsonRpc("/blog/post_discussion", 'call', {
            'blog_post_id': self.settings.post_id,
            'path': self.discus_identifier,
            'comment': val[0],
        }).then(function(res){
            $(".popover ul.media-list").prepend(qweb.render("website.blog_discussion.comment", {'res': res[0]}));
            var ele = $('a[data-discus-identifier="'+ self.discus_identifier +'"]');
            ele.text(_.isNaN(parseInt(ele.text())) ? 1 : parseInt(ele.text())+1);
            ele.addClass('has-comments');
        });
    },
    hide_discussion : function() {
        var self =  this;
        $('a[data-discus-identifier="'+ self.discus_identifier+'"]').popover('destroy');
        $('a.discussion-link').removeClass('active');
    }

});

return InlineDiscussion;

});
Beispiel #29
0
odoo.define('web_tour.TourManager', function(require) {
"use strict";

var core = require('web.core');
var local_storage = require('web.local_storage');
var Model = require('web.Model');
var session = require('web.session');
var Tip = require('web_tour.Tip');

var _t = core._t;

var RUNNING_TOUR_TIMEOUT = 10000;

function get_step_key(name) {
    return 'tour_' + name + '_step';
}
function get_running_key() {
    return 'running_tour';
}
function get_running_delay_key() {
    return get_running_key() + "_delay";
}

var RunningTourActionHelper = core.Class.extend({
    init: function (tip_widget) {
        this.tip_widget = tip_widget;
    },
    click: function (element) {
        this._click(this._get_action_values(element));
    },
    text: function (text, element) {
        this._text(this._get_action_values(element), text);
    },
    drag_and_drop: function (to, element) {
        this._drag_and_drop(this._get_action_values(element), to);
    },
    auto: function (element) {
        var values = this._get_action_values(element);
        if (values.consume_event === "input") {
            this._text(values);
        } else {
            this._click(values);
        }
    },
    _get_action_values: function (element) {
        var $element = element ? $(element).first() : this.tip_widget.$anchor;
        var consume_event = element ? Tip.getConsumeEventType($element) : this.tip_widget.consume_event;
        return {
            $element: $element,
            consume_event: consume_event,
        };
    },
    _click: function (values) {
        var href = values.$element.attr("href");
        if (href && href.length && href[0] !== "#" && values.$element.is("a")) {
            window.location.href = href;
        } else {
            values.$element.mousedown().mouseup().click();
        }
    },
    _text: function (values, text) {
        this._click(values);

        text = text || "Test";
        if (values.consume_event === "input") {
            values.$element.val(text).trigger("input");
        } else {
            values.$element.text(text);
        }
    },
    _drag_and_drop: function (values, to) {
        var $to = $(to || document.body);

        var elementCenter = values.$element.offset();
        elementCenter.left += values.$element.outerWidth()/2;
        elementCenter.top += values.$element.outerHeight()/2;

        var toCenter = $to.offset();
        toCenter.left += $to.outerWidth()/2;
        toCenter.top += $to.outerHeight()/2;

        values.$element.trigger($.Event("mousedown", {which: 1, pageX: elementCenter.left, pageY: elementCenter.top}));
        values.$element.trigger($.Event("mousemove", {which: 1, pageX: toCenter.left, pageY: toCenter.top}));
        values.$element.trigger($.Event("mouseup", {which: 1, pageX: toCenter.left, pageY: toCenter.top}));
    },
});

return core.Class.extend({
    init: function(consumed_tours) {
        this.$body = $('body');
        this.active_tooltips = {};
        this.tours = {};
        this.consumed_tours = consumed_tours || [];
        this.running_tour = local_storage.getItem(get_running_key());
        this.running_step_delay = parseInt(local_storage.getItem(get_running_delay_key()), 10) || 300;
        this.TourModel = new Model('web_tour.tour');
        this.edition = (_.last(session.server_version_info) === 'e') ? 'enterprise' : 'community';
    },
    /**
     * Registers a tour described by the following arguments (in order)
     * @param [String] tour's name
     * @param [Object] dict of options (optional), available options are:
     *   test [Boolean] true if the tour is dedicated to tests (it won't be enabled by default)
     *   skip_enabled [Boolean] true to add a link to consume the whole tour in its tips
     *   url [String] the url to load when manually running the tour
     * @param [Array] dict of steps, each step being a dict containing a tip description
     */
    register: function() {
        var self = this;
        var args = Array.prototype.slice.call(arguments);
        var last_arg = args[args.length - 1];
        var name = args[0];
        if (this.tours[name]) {
            console.warn(_.str.sprintf("Tour %s is already defined", name));
            return;
        }
        var options = args.length === 2 ? {} : args[1];
        var steps = last_arg instanceof Array ? last_arg : [last_arg];
        var tour = {
            name: name,
            current_step: parseInt(local_storage.getItem(get_step_key(name))) || 0,
            steps: _.filter(steps, function (step) {
                return !step.edition || step.edition === self.edition;
            }),
            url: options.url,
            test: options.test,
        };
        if (options.skip_enabled) {
            tour.skip_link = '<p><span class="o_skip_tour">' + _t('Skip tour') + '</span></p>';
            tour.skip_handler = function (tip) {
                this._deactivate_tip(tip);
                this._consume_tour(name);
            };
        }
        this.tours[name] = tour;
        if (this.running_tour === name || (!tour.test && !_.contains(this.consumed_tours, name))) {
            this._to_next_step(name, 0);
        }

        if (!this.running_tour || this.running_tour === name) {
            this.update(name);
        }
    },
    run: function (tour_name, step_delay) {
        if (this.running_tour) {
            console.warn(_.str.sprintf("Killing tour %s", tour_name));
            this._deactivate_tip(this.active_tooltips[tour_name]);
            this._consume_tour(tour_name);
            return;
        }
        var tour = this.tours[tour_name];
        if (!tour) {
            console.warn(_.str.sprintf("Unknown Tour %s", name));
            return;
        }
        console.log(_.str.sprintf("Running tour %s", tour_name));
        this.running_tour = tour_name;
        this.running_step_delay = step_delay || this.running_step_delay;
        local_storage.setItem(get_running_key(), this.running_tour);
        local_storage.setItem(get_running_delay_key(), this.running_step_delay);

        this._deactivate_tip(this.active_tooltips[tour_name]);

        tour.current_step = 0;
        local_storage.setItem(get_step_key(tour_name), tour.current_step);
        this.active_tooltips[tour_name] = tour.steps[tour.current_step];

        if (tour.url) {
            this.pause();
            var old_before = window.onbeforeunload;
            var reload_timeout;
            window.onbeforeunload = function () {
                clearTimeout(reload_timeout);
            };
            reload_timeout = _.defer((function () {
                window.onbeforeunload = old_before;
                this.play();
                this.update();
            }).bind(this));

            window.location.href = session.debug ? $.param.querystring(tour.url, {debug: session.debug}) : tour.url;
        } else {
            this.update();
        }
    },
    pause: function () {
        this.paused = true;
    },
    play: function () {
        this.paused = false;
    },
    /**
     * Checks for tooltips to activate (only from the running tour or specified tour if there
     * is one, from all active tours otherwise). Should be called each time the DOM changes.
     */
    update: function (tour_name) {
        if (this.paused) return;

        if (this.running_tour) {
            if (this.tours[this.running_tour] === undefined) return;
            if (this.running_tour_timeout === undefined) {
                this._set_running_tour_timeout(this.running_tour, this.active_tooltips[this.running_tour]);
            }
        }

        this.$modal_displayed = $('.modal:visible').last();
        tour_name = this.running_tour || tour_name;
        if (tour_name) {
            this._check_for_tooltip(this.active_tooltips[tour_name], tour_name);
        } else {
            _.each(this.active_tooltips, this._check_for_tooltip.bind(this));
        }
    },
    _check_for_tooltip: function (tip, tour_name) {
        var $trigger;
        if (this.$modal_displayed.length) {
            $trigger = this.$modal_displayed.find(tip.trigger);
        } else {
            $trigger = $(tip.trigger);
        }
        $trigger = $trigger.filter(':visible').first();
        var extra_trigger = tip.extra_trigger ? $(tip.extra_trigger).filter(':visible').length : true;
        var triggered = $trigger.length && extra_trigger;
        if (triggered) {
            if (!tip.widget) {
                this._activate_tip(tip, tour_name, $trigger);
            } else {
                tip.widget.update($trigger);
            }
        } else {
            this._deactivate_tip(tip);
        }
    },
    _activate_tip: function(tip, tour_name, $anchor) {
        var tour = this.tours[tour_name];
        var tip_info = tip;
        if (tour.skip_link) {
            tip_info = _.extend(_.omit(tip_info, 'content'), {
                content: tip.content + tour.skip_link,
                event_handlers: [{
                    event: 'click',
                    selector: '.o_skip_tour',
                    handler: tour.skip_handler.bind(this, tip),
                }],
            });
        }
        tip.widget = new Tip(this, tip_info);
        if (this.running_tour !== tour_name) {
            tip.widget.on('tip_consumed', this, this._consume_tip.bind(this, tip, tour_name));
        }
        tip.widget.attach_to($anchor).then(this._to_next_running_step.bind(this, tip, tour_name));
    },
    _deactivate_tip: function(tip) {
        if (tip && tip.widget) {
            tip.widget.destroy();
            delete tip.widget;
        }
    },
    _consume_tip: function(tip, tour_name) {
        this._deactivate_tip(tip);
        this._to_next_step(tour_name);

        var is_running = (this.running_tour === tour_name);
        if (is_running) {
            console.log(_.str.sprintf("Tour %s: step %s succeeded", tour_name, tip.trigger));
        }

        if (this.active_tooltips[tour_name]) {
            local_storage.setItem(get_step_key(tour_name), this.tours[tour_name].current_step);
            if (is_running) {
                this._set_running_tour_timeout(tour_name, this.active_tooltips[tour_name]);
            }
            this.update(tour_name);
        } else {
            this._consume_tour(tour_name);
        }
    },
    _to_next_step: function (tour_name, inc) {
        var tour = this.tours[tour_name];
        tour.current_step += (inc !== undefined ? inc : 1);
        if (this.running_tour !== tour_name) {
            var index = _.findIndex(tour.steps.slice(tour.current_step), function (tip) {
                return !tip.auto;
            });
            if (index >= 0) {
                tour.current_step += index;
            } else {
                tour.current_step = tour.steps.length;
            }
        }
        this.active_tooltips[tour_name] = tour.steps[tour.current_step];
    },
    _consume_tour: function (tour_name, error) {
        delete this.active_tooltips[tour_name];
        this.tours[tour_name].current_step = 0;
        local_storage.removeItem(get_step_key(tour_name));
        if (this.running_tour === tour_name) {
            this._stop_running_tour_timeout();
            local_storage.removeItem(get_running_key());
            local_storage.removeItem(get_running_delay_key());
            this.running_tour = undefined;
            this.running_step_delay = undefined;
            if (error) {
                console.log("error " + error); // phantomJS wait for message starting by error
            } else {
                console.log(_.str.sprintf("Tour %s succeeded", tour_name));
                console.log("ok"); // phantomJS wait for exact message "ok"
            }
        } else {
            this.TourModel.call('consume', [[tour_name]]).then((function () {
                this.consumed_tours.push(tour_name);
            }).bind(this));
        }
    },
    _set_running_tour_timeout: function (tour_name, step) {
        this._stop_running_tour_timeout();
        this.running_tour_timeout = setTimeout((function() {
            this._consume_tour(tour_name, _.str.sprintf("Tour %s failed at step %s", tour_name, step.trigger));
        }).bind(this), RUNNING_TOUR_TIMEOUT + this.running_step_delay);
    },
    _stop_running_tour_timeout: function () {
        clearTimeout(this.running_tour_timeout);
        this.running_tour_timeout = undefined;
    },
    _to_next_running_step: function (tip, tour_name) {
        if (this.running_tour !== tour_name) return;
        this._stop_running_tour_timeout();

        var action_helper = new RunningTourActionHelper(tip.widget);
        _.delay((function () {
            if (typeof tip.run === "function") {
                tip.run.call(tip.widget, action_helper);
            } else if (tip.run !== undefined) {
                var m = tip.run.match(/^(click|text|drag_and_drop) *(?:\(? *["']?(.+)["']? *\)?)?$/);
                action_helper[m[1]](m[2]);
            } else {
                action_helper.auto();
            }
            this._consume_tip(tip, tour_name);
        }).bind(this), this.running_step_delay);
    },

    /**
     * Tour predefined steps
     */
    STEPS: {
        MENU_MORE: {
            edition: "community",
            trigger: "body > nav",
            position: "bottom",
            auto: true,
            run: function (actions) {
                actions.auto("#menu_more_container > a");
            },
        },

        TOGGLE_APPSWITCHER: {
            edition: "enterprise",
            trigger: ".o_main_navbar .o_menu_toggle",
            content: _t('Click the <i>Home icon</i> to navigate across apps.'),
            position: "bottom",
        },

        WEBSITE_NEW_PAGE: {
            trigger: "#oe_main_menu_navbar a[data-action=new_page]",
            auto: true,
            position: "bottom",
        },
    },
});

});