Exemplo n.º 1
0
odoo.define('point_of_sale.chrome', function (require) {
"use strict";

var PosBaseWidget = require('point_of_sale.BaseWidget');
var gui = require('point_of_sale.gui');
var keyboard = require('point_of_sale.keyboard');
var models = require('point_of_sale.models');
var AbstractAction = require('web.AbstractAction');
var core = require('web.core');
var ajax = require('web.ajax');
var CrashManager = require('web.CrashManager');
var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;


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

/* -------- The Order Selector -------- */

// Allows the cashier to create / delete and
// switch between orders.

var OrderSelectorWidget = PosBaseWidget.extend({
    template: 'OrderSelectorWidget',
    init: function(parent, options) {
        this._super(parent, options);
        this.pos.get('orders').bind('add remove change',this.renderElement,this);
        this.pos.bind('change:selectedOrder',this.renderElement,this);
    },
    get_order_by_uid: function(uid) {
        var orders = this.pos.get_order_list();
        for (var i = 0; i < orders.length; i++) {
            if (orders[i].uid === uid) {
                return orders[i];
            }
        }
        return undefined;
    },
    order_click_handler: function(event,$el) {
        var order = this.get_order_by_uid($el.data('uid'));
        if (order) {
            this.pos.set_order(order);
        }
    },
    neworder_click_handler: function(event, $el) {
        this.pos.add_new_order();
    },
    deleteorder_click_handler: function(event, $el) {
        var self  = this;
        var order = this.pos.get_order(); 
        if (!order) {
            return;
        } else if ( !order.is_empty() ){
            this.gui.show_popup('confirm',{
                'title': _t('Destroy Current Order ?'),
                'body': _t('You will lose any data associated with the current order'),
                confirm: function(){
                    self.pos.delete_current_order();
                },
            });
        } else {
            this.pos.delete_current_order();
        }
    },
    renderElement: function(){
        var self = this;
        this._super();
        this.$('.order-button.select-order').click(function(event){
            self.order_click_handler(event,$(this));
        });
        this.$('.neworder-button').click(function(event){
            self.neworder_click_handler(event,$(this));
        });
        this.$('.deleteorder-button').click(function(event){
            self.deleteorder_click_handler(event,$(this));
        });
    },
});

/* ------- The User Name Widget ------- */

// Displays the current cashier's name

var UsernameWidget = PosBaseWidget.extend({
    template: 'UsernameWidget',
    init: function(parent, options){
        options = options || {};
        this._super(parent,options);
    },
    get_name: function(){
        var user = this.pos.get_cashier();
        if(user){
            return user.name;
        }else{
            return "";
        }
    },
});

/* -------- The Header Button --------- */

// Used to quickly add buttons with simple
// labels and actions to the point of sale 
// header.

var HeaderButtonWidget = PosBaseWidget.extend({
    template: 'HeaderButtonWidget',
    init: function(parent, options){
        options = options || {};
        this._super(parent, options);
        this.action = options.action;
        this.label  = options.label;
        this.button_class = options.button_class;

    },
    renderElement: function(){
        var self = this;
        this._super();
        if(this.action){
            this.$el.click(function(){
                self.action();
            });
        }
    },
    show: function() { this.$el.removeClass('oe_hidden'); },
    hide: function() { this.$el.addClass('oe_hidden'); },
});

/* --------- The Debug Widget --------- */

// The debug widget lets the user control 
// and monitor the hardware and software status
// without the use of the proxy, or to access
// the raw locally stored db values, useful
// for debugging

var DebugWidget = PosBaseWidget.extend({
    template: "DebugWidget",
    eans:{
        admin_badge:  '0410100000006',
        client_badge: '0420200000004',
        invalid_ean:  '1232456',
        soda_33cl:    '5449000000996',
        oranges_kg:   '2100002031410',
        lemon_price:  '2301000001560',
        unknown_product: '9900000000004',
    },
    events:[
        'open_cashbox',
        'print_receipt',
        'scale_read',
    ],
    init: function(parent,options){
        this._super(parent,options);
        var self = this;
        
        // for dragging the debug widget around
        this.dragging  = false;
        this.dragpos = {x:0, y:0};

        function eventpos(event){
            if(event.touches && event.touches[0]){
                return {x: event.touches[0].screenX, y: event.touches[0].screenY};
            }else{
                return {x: event.screenX, y: event.screenY};
            }
        }

        this.dragend_handler = function(event){
            self.dragging = false;
        };
        this.dragstart_handler = function(event){
            self.dragging = true;
            self.dragpos = eventpos(event);
        };
        this.dragmove_handler = function(event){
            if(self.dragging){
                var top = this.offsetTop;
                var left = this.offsetLeft;
                var pos  = eventpos(event);
                var dx   = pos.x - self.dragpos.x; 
                var dy   = pos.y - self.dragpos.y; 

                self.dragpos = pos;

                this.style.right = 'auto';
                this.style.bottom = 'auto';
                this.style.left = left + dx + 'px';
                this.style.top  = top  + dy + 'px';
            }
            event.preventDefault();
            event.stopPropagation();
        };
    },
    show: function() {
        this.$el.css({opacity:0});
        this.$el.removeClass('oe_hidden');
        this.$el.animate({opacity:1},250,'swing');
    },
    hide: function() {
        var self = this;
        this.$el.animate({opacity:0,},250,'swing',function(){
            self.$el.addClass('oe_hidden');
        });
    },
    start: function(){
        var self = this;
        
        if (this.pos.debug) {
            this.show();
        }

        this.el.addEventListener('mouseleave', this.dragend_handler);
        this.el.addEventListener('mouseup',    this.dragend_handler);
        this.el.addEventListener('touchend',   this.dragend_handler);
        this.el.addEventListener('touchcancel',this.dragend_handler);
        this.el.addEventListener('mousedown',  this.dragstart_handler);
        this.el.addEventListener('touchstart', this.dragstart_handler);
        this.el.addEventListener('mousemove',  this.dragmove_handler);
        this.el.addEventListener('touchmove',  this.dragmove_handler);

        this.$('.toggle').click(function(){
            self.hide();
        });
        this.$('.button.set_weight').click(function(){
            var kg = Number(self.$('input.weight').val());
            if(!isNaN(kg)){
                self.pos.proxy.debug_set_weight(kg);
            }
        });
        this.$('.button.reset_weight').click(function(){
            self.$('input.weight').val('');
            self.pos.proxy.debug_reset_weight();
        });
        this.$('.button.custom_ean').click(function(){
            var ean = self.pos.barcode_reader.barcode_parser.sanitize_ean(self.$('input.ean').val() || '0');
            self.$('input.ean').val(ean);
            self.pos.barcode_reader.scan(ean);
        });
        this.$('.button.barcode').click(function(){
            self.pos.barcode_reader.scan(self.$('input.ean').val());
        });
        this.$('.button.delete_orders').click(function(){
            self.gui.show_popup('confirm',{
                'title': _t('Delete Paid Orders ?'),
                'body':  _t('This operation will permanently destroy all paid orders from the local storage. You will lose all the data. This operation cannot be undone.'),
                confirm: function(){
                    self.pos.db.remove_all_orders();
                    self.pos.set({synch: { state:'connected', pending: 0 }});
                },
            });
        });
        this.$('.button.delete_unpaid_orders').click(function(){
            self.gui.show_popup('confirm',{
                'title': _t('Delete Unpaid Orders ?'),
                'body':  _t('This operation will destroy all unpaid orders in the browser. You will lose all the unsaved data and exit the point of sale. This operation cannot be undone.'),
                confirm: function(){
                    self.pos.db.remove_all_unpaid_orders();
                    window.location = '/';
                },
            });
        });

        this.$('.button.export_unpaid_orders').click(function(){
            self.gui.prepare_download_link(
                self.pos.export_unpaid_orders(),
                _t("unpaid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json',
                ".export_unpaid_orders", ".download_unpaid_orders"
            );
        });

        this.$('.button.export_paid_orders').click(function() {
            self.gui.prepare_download_link(
                self.pos.export_paid_orders(),
                _t("paid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json',
                ".export_paid_orders", ".download_paid_orders"
            );
        });

        this.$('.button.display_refresh').click(function () {
            self.pos.proxy.message('display_refresh', {});
        });

        this.$('.button.import_orders input').on('change', function(event) {
            var file = event.target.files[0];

            if (file) {
                var reader = new FileReader();
                
                reader.onload = function(event) {
                    var report = self.pos.import_orders(event.target.result);
                    self.gui.show_popup('orderimport',{report:report});
                };
                
                reader.readAsText(file);
            }
        });

        _.each(this.events, function(name){
            self.pos.proxy.add_notification(name,function(){
                self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'}); 
                self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
            });
        });
    },
});

/* --------- The Status Widget -------- */

// Base class for widgets that want to display
// status in the point of sale header.

var StatusWidget = PosBaseWidget.extend({
    status: ['connected','connecting','disconnected','warning','error'],

    set_status: function(status,msg){
        for(var i = 0; i < this.status.length; i++){
            this.$('.js_'+this.status[i]).addClass('oe_hidden');
        }
        this.$('.js_'+status).removeClass('oe_hidden');
        
        if(msg){
            this.$('.js_msg').removeClass('oe_hidden').html(msg);
        }else{
            this.$('.js_msg').addClass('oe_hidden').html('');
        }
    },
});

/* ------- Synch. Notifications ------- */

// Displays if there are orders that could
// not be submitted, and how many. 

var SynchNotificationWidget = StatusWidget.extend({
    template: 'SynchNotificationWidget',
    start: function(){
        var self = this;
        this.pos.bind('change:synch', function(pos,synch){
            self.set_status(synch.state, synch.pending);
        });
        this.$el.click(function(){
            self.pos.push_order(null,{'show_error':true});
        });
    },
});

/* --------- The Proxy Status --------- */

// Displays the status of the hardware proxy
// (connected, disconnected, errors ... )

var ProxyStatusWidget = StatusWidget.extend({
    template: 'ProxyStatusWidget',
    set_smart_status: function(status){
        if(status.status === 'connected'){
            var warning = false;
            var msg = '';
            if(this.pos.config.iface_scan_via_proxy){
                var scanner = status.drivers.scanner ? status.drivers.scanner.status : false;
                if( scanner != 'connected' && scanner != 'connecting'){
                    warning = true;
                    msg += _t('Scanner');
                }
            }
            if( this.pos.config.iface_print_via_proxy || 
                this.pos.config.iface_cashdrawer ){
                var printer = status.drivers.escpos ? status.drivers.escpos.status : false;
                if( printer != 'connected' && printer != 'connecting'){
                    warning = true;
                    msg = msg ? msg + ' & ' : msg;
                    msg += _t('Printer');
                }
            }
            if( this.pos.config.iface_electronic_scale ){
                var scale = status.drivers.scale ? status.drivers.scale.status : false;
                if( scale != 'connected' && scale != 'connecting' ){
                    warning = true;
                    msg = msg ? msg + ' & ' : msg;
                    msg += _t('Scale');
                }
            }

            msg = msg ? msg + ' ' + _t('Offline') : msg;
            this.set_status(warning ? 'warning' : 'connected', msg);
        }else{
            this.set_status(status.status,'');
        }
    },
    start: function(){
        var self = this;
        
        this.set_smart_status(this.pos.proxy.get('status'));

        this.pos.proxy.on('change:status',this,function(eh,status){ //FIXME remove duplicate changes 
            self.set_smart_status(status.newValue);
        });

        this.$el.click(function(){
            self.pos.connect_to_proxy();
        });
    },
});


/* --------- The Sale Details --------- */

// Generates a report to print the sales of the
// day on a ticket

var SaleDetailsButton = PosBaseWidget.extend({
    template: 'SaleDetailsButton',
    start: function(){
        var self = this;
        this.$el.click(function(){
            self.pos.proxy.print_sale_details();
        });
    },
});

/* User interface for distant control over the Client display on the IoT Box */
// The boolean posbox_supports_display (in devices.js) will allow interaction to the IoT Box on true, prevents it otherwise
// We don't want the incompatible IoT Box to be flooded with 404 errors on arrival of our many requests as it triggers losses of connections altogether
var ClientScreenWidget = PosBaseWidget.extend({
    template: 'ClientScreenWidget',

    change_status_display: function(status) {
        var msg = ''
        if (status === 'success') {
            this.$('.js_warning').addClass('oe_hidden');
            this.$('.js_disconnected').addClass('oe_hidden');
            this.$('.js_connected').removeClass('oe_hidden');
        } else if (status === 'warning') {
            this.$('.js_disconnected').addClass('oe_hidden');
            this.$('.js_connected').addClass('oe_hidden');
            this.$('.js_warning').removeClass('oe_hidden');
            msg = _t('Connected, Not Owned');
        } else {
            this.$('.js_warning').addClass('oe_hidden');
            this.$('.js_connected').addClass('oe_hidden');
            this.$('.js_disconnected').removeClass('oe_hidden');
            msg = _t('Disconnected')
            if (status === 'not_found') {
                msg = _t('Client Screen Unsupported. Please upgrade the IoT Box')
            }
        }

        this.$('.oe_customer_display_text').text(msg);
    },

    status_loop: function() {
        var self = this;
        function loop() {
            if (self.pos.proxy.posbox_supports_display) {
                self.pos.proxy.test_ownership_of_client_screen().then(
                    function (data) {
                        if (typeof data === 'string') {
                            data = JSON.parse(data);
                        }
                        if (data.status === 'OWNER') {
                            self.change_status_display('success');
                        } else {
                            self.change_status_display('warning');
                        }
                        setTimeout(loop, 3000);
                    },
                    function (err) {
                        if (err.abort) {
                            // Stop the loop
                            return;
                        }
                        if (typeof err == "undefined") {
                            self.change_status_display('failure');
                        } else {
                            self.change_status_display('not_found');
                            self.pos.proxy.posbox_supports_display = false;
                        }
                        setTimeout(loop, 3000);
                    }
                );
            }
        }
        loop();
    },

    start: function(){
        if (this.pos.config.iface_customer_facing_display) {
                this.show();
                var self = this;
                this.$el.click(function(){
                    self.pos.render_html_for_customer_facing_display().then(function(rendered_html) {
                        self.pos.proxy.take_ownership_over_client_screen(rendered_html).then(
                        function(data) {
                            if (typeof data === 'string') {
                                data = JSON.parse(data);
                            }
                            if (data.status === 'success') {
                               self.change_status_display('success');
                            } else {
                               self.change_status_display('warning');
                            }
                            if (!self.pos.proxy.posbox_supports_display) {
                                self.pos.proxy.posbox_supports_display = true;
                                self.status_loop();
                            }
                        }, 
        
                        function(err) {
                            if (typeof err == "undefined") {
                                self.change_status_display('failure');
                            } else {
                                self.change_status_display('not_found');
                            }
                        });
                    });

                });
                this.status_loop();
        } else {
            this.hide();
        }
    },
});


/*--------------------------------------*\
 |             THE CHROME               |
\*======================================*/

// The Chrome is the main widget that contains 
// all other widgets in the PointOfSale.
//
// It is the first object instanciated and the
// starting point of the point of sale code.
//
// It is mainly composed of :
// - a header, containing the list of orders
// - a leftpane, containing the list of bought 
//   products (orderlines) 
// - a rightpane, containing the screens 
//   (see pos_screens.js)
// - popups
// - an onscreen keyboard
// - .gui which controls the switching between 
//   screens and the showing/closing of popups

var Chrome = PosBaseWidget.extend(AbstractAction.prototype, {
    template: 'Chrome',
    init: function() { 
        var self = this;
        this._super(arguments[0],{});

        this.started  = new $.Deferred(); // resolves when DOM is online
        this.ready    = new $.Deferred(); // resolves when the whole GUI has been loaded

        this.pos = new models.PosModel(this.getSession(), {chrome:this});
        this.gui = new gui.Gui({pos: this.pos, chrome: this});
        this.chrome = this; // So that chrome's childs have chrome set automatically
        this.pos.gui = this.gui;

        this.logo_click_time  = 0;
        this.logo_click_count = 0;

        this.previous_touch_y_coordinate = -1;

        this.widget = {};   // contains references to subwidgets instances

        this.cleanup_dom();
        this.pos.ready.then(function(){
            self.build_chrome();
            self.build_widgets();
            self.disable_rubberbanding();
            self.disable_backpace_back();
            self.ready.resolve();
            self.loading_hide();
            self.replace_crashmanager();
            self.pos.push_order();
        }).guardedCatch(function (err) { // error when loading models data from the backend
            self.loading_error(err);
        });
    },

    cleanup_dom:  function() {
        // remove default webclient handlers that induce click delay
        $(document).off();
        $(window).off();
        $('html').off();
        $('body').off();
        // The above lines removed the bindings, but we really need them for the barcode
        BarcodeEvents.start();
    },

    build_chrome: function() { 
        var self = this;

        if ($.browser.chrome) {
            var chrome_version = $.browser.version.split('.')[0];
            if (parseInt(chrome_version, 10) >= 50) {
                ajax.loadCSS('/point_of_sale/static/src/css/chrome50.css');
            }
        }

        this.renderElement();

        this.$('.pos-logo').click(function(){
            self.click_logo();
        });

        if(this.pos.config.iface_big_scrollbars){
            this.$el.addClass('big-scrollbars');
        }
    },

    // displays a system error with the error-traceback
    // popup.
    show_error: function(error) {
        this.gui.show_popup('error-traceback',{
            'title': error.message,
            'body':  error.message + '\n' + error.data.debug + '\n',
        });
    },

    // replaces the error handling of the existing crashmanager which
    // uses jquery dialog to display the error, to use the pos popup
    // instead
    replace_crashmanager: function() {
        var self = this;
        CrashManager.include({
            show_error: function(error) {
                if (self.gui) {
                    self.show_error(error);
                } else {
                    this._super(error);
                }
            },
        });
    },

    click_logo: function() {
        if (this.pos.debug) {
            this.widget.debug.show();
        } else {
            var self  = this;
            var time  = (new Date()).getTime();
            var delay = 500;
            if (this.logo_click_time + 500 < time) {
                this.logo_click_time  = time;
                this.logo_click_count = 1;
            } else {
                this.logo_click_time  = time;
                this.logo_click_count += 1;
                if (this.logo_click_count >= 6) {
                    this.logo_click_count = 0;
                    this.gui.sudo().then(function(){
                        self.widget.debug.show();
                    });
                }
            }
        }
    },

        _scrollable: function(element, scrolling_down){
            var $element = $(element);
            var scrollable = true;

            if (! scrolling_down && $element.scrollTop() <= 0) {
                scrollable = false;
            } else if (scrolling_down && $element.scrollTop() + $element.height() >= element.scrollHeight) {
                scrollable = false;
            }

            return scrollable;
        },

    disable_rubberbanding: function(){
            var self = this;

            document.body.addEventListener('touchstart', function(event){
                self.previous_touch_y_coordinate = event.touches[0].clientY;
            });

        // prevent the pos body from being scrollable. 
        document.body.addEventListener('touchmove',function(event){
            var node = event.target;
                var current_touch_y_coordinate = event.touches[0].clientY;
                var scrolling_down;

                if (current_touch_y_coordinate < self.previous_touch_y_coordinate) {
                    scrolling_down = true;
                } else {
                    scrolling_down = false;
                }

            while(node){
                if(node.classList && node.classList.contains('touch-scrollable') && self._scrollable(node, scrolling_down)){
                    return;
                }
                node = node.parentNode;
            }
            event.preventDefault();
        });
    },

    // prevent backspace from performing a 'back' navigation
    disable_backpace_back: function() {
       $(document).on("keydown", function (e) {
           if (e.which === 8 && !$(e.target).is("input, textarea")) {
               e.preventDefault();
           }
       });
    },

    loading_error: function(err){
        var self = this;

        var title = err.message;
        var body  = err.stack;

        if(err.message === 'XmlHttpRequestError '){
            title = 'Network Failure (XmlHttpRequestError)';
            body  = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
        }else if(err.code === 200){
            title = err.data.message;
            body  = err.data.debug;
        }

        if( typeof body !== 'string' ){
            body = 'Traceback not available.';
        }

        var popup = $(QWeb.render('ErrorTracebackPopupWidget',{
            widget: { options: {title: title , body: body }},
        }));

        popup.find('.button').click(function(){
            self.gui.close();
        });

        popup.css({ zindex: 9001 });

        popup.appendTo(this.$el);
    },
    loading_progress: function(fac){
        this.$('.loader .loader-feedback').removeClass('oe_hidden');
        this.$('.loader .progress').removeClass('oe_hidden').css({'width': ''+Math.floor(fac*100)+'%'});
    },
    loading_message: function(msg,progress){
        this.$('.loader .loader-feedback').removeClass('oe_hidden');
        this.$('.loader .message').text(msg);
        if (typeof progress !== 'undefined') {
            this.loading_progress(progress);
        } else {
            this.$('.loader .progress').addClass('oe_hidden');
        }
    },
    loading_skip: function(callback){
        if(callback){
            this.$('.loader .loader-feedback').removeClass('oe_hidden');
            this.$('.loader .button.skip').removeClass('oe_hidden');
            this.$('.loader .button.skip').off('click');
            this.$('.loader .button.skip').click(callback);
        }else{
            this.$('.loader .button.skip').addClass('oe_hidden');
        }
    },
    loading_hide: function(){
        var self = this;
        this.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
    },
    loading_show: function(){
        this.$('.loader').removeClass('oe_hidden').animate({opacity:1},150,'swing');
    },
    widgets: [
        {
            'name':   'order_selector',
            'widget': OrderSelectorWidget,
            'replace':  '.placeholder-OrderSelectorWidget',
        },{
            'name':   'sale_details',
            'widget': SaleDetailsButton,
            'append':  '.pos-rightheader',
            'condition': function(){ return this.pos.config.use_proxy; },
        },{
            'name':   'proxy_status',
            'widget': ProxyStatusWidget,
            'append':  '.pos-rightheader',
            'condition': function(){ return this.pos.config.use_proxy; },
        },{
            'name': 'screen_status',
            'widget': ClientScreenWidget,
            'append': '.pos-rightheader',
            'condition': function(){ return this.pos.config.use_proxy; },
        },{
            'name':   'notification',
            'widget': SynchNotificationWidget,
            'append':  '.pos-rightheader',
        },{
            'name':   'close_button',
            'widget': HeaderButtonWidget,
            'append':  '.pos-rightheader',
            'args': {
                label: _t('Close'),
                action: function(){ 
                    this.$el.addClass('close_button');
                    var self = this;
                    if (!this.confirmed) {
                        this.$el.addClass('confirm');
                        this.$el.text(_t('Confirm'));
                        this.confirmed = setTimeout(function(){
                            self.$el.removeClass('confirm');
                            self.$el.text(_t('Close'));
                            self.confirmed = false;
                        },2000);
                    } else {
                        clearTimeout(this.confirmed);
                        this.gui.close();
                    }
                },
            }
        },{
            'name':   'username',
            'widget': UsernameWidget,
            'replace':  '.placeholder-UsernameWidget',
        },{
            'name':  'keyboard',
            'widget': keyboard.OnscreenKeyboardWidget,
            'replace': '.placeholder-OnscreenKeyboardWidget',
        },{
            'name':  'debug',
            'widget': DebugWidget,
            'append': '.pos-content',
        },
    ],

    load_widgets: function(widgets) {
        for (var i = 0; i < widgets.length; i++) {
            var widget = widgets[i];
            if ( !widget.condition || widget.condition.call(this) ) {
                var args = typeof widget.args === 'function' ? widget.args(this) : widget.args;
                var w = new widget.widget(this, args || {});
                if (widget.replace) {
                    w.replace(this.$(widget.replace));
                } else if (widget.append) {
                    w.appendTo(this.$(widget.append));
                } else if (widget.prepend) {
                    w.prependTo(this.$(widget.prepend));
                } else {
                    w.appendTo(this.$el);
                }
                this.widget[widget.name] = w;
            }
        }
    },

    // This method instantiates all the screens, widgets, etc.
    build_widgets: function() {
        var self = this;
        this.load_widgets(this.widgets);

        this.screens = {};
        var classe;
        for (var i = 0; i < this.gui.screen_classes.length; i++) {
            classe = this.gui.screen_classes[i];
            if (!classe.condition || classe.condition.call(this)) {
                var screen = new classe.widget(this,{});
                    screen.appendTo(this.$('.screens'));
                this.screens[classe.name] = screen;
                this.gui.add_screen(classe.name, screen);
            }
        }

        this.popups = {};
        _.forEach(this.gui.popup_classes, function (classe) {
            if (!classe.condition || classe.condition.call(self)) {
                var popup = new classe.widget(self,{});
                popup.appendTo(self.$('.popups')).then(function () {
                    self.popups[classe.name] = popup;
                    self.gui.add_popup(classe.name, popup);
                });
            }
        });

        this.gui.set_startup_screen('products');
        this.gui.set_default_screen('products');

    },

    destroy: function() {
        this.pos.destroy();
        this._super();
    }
});

return {
    Chrome: Chrome,
    DebugWidget: DebugWidget,
    HeaderButtonWidget: HeaderButtonWidget,
    OrderSelectorWidget: OrderSelectorWidget,
    ProxyStatusWidget: ProxyStatusWidget,
    SaleDetailsButton: SaleDetailsButton,
    ClientScreenWidget: ClientScreenWidget,
    StatusWidget: StatusWidget,
    SynchNotificationWidget: SynchNotificationWidget,
    UsernameWidget: UsernameWidget,
};
});
Exemplo n.º 2
0
odoo.define('point_of_sale.popups', function (require) {
"use strict";

// This file contains the Popups.
// Popups must be loaded and named in chrome.js. 
// They are instanciated / destroyed with the .gui.show_popup()
// and .gui.close_popup() methods.

var PosBaseWidget = require('point_of_sale.BaseWidget');
var gui = require('point_of_sale.gui');
var _t  = require('web.core')._t;


var PopupWidget = PosBaseWidget.extend({
    template: 'PopupWidget',
    init: function(parent, args) {
        this._super(parent, args);
        this.options = {};
    },
    events: {
        'click .button.cancel':  'click_cancel',
        'click .button.confirm': 'click_confirm',
        'click .selection-item': 'click_item',
        'click .input-button':   'click_numpad',
        'click .mode-button':    'click_numpad',
    },

    // show the popup !  
    show: function(options){
        if(this.$el){
            this.$el.removeClass('oe_hidden');
        }
        
        if (typeof options === 'string') {
            this.options = {title: options};
        } else {
            this.options = options || {};
        }

        this.renderElement();

        // popups block the barcode reader ... 
        if (this.pos.barcode_reader) {
            this.pos.barcode_reader.save_callbacks();
            this.pos.barcode_reader.reset_action_callbacks();
        }
    },

    // called before hide, when a popup is closed.
    // extend this if you want a custom action when the 
    // popup is closed.
    close: function(){
        if (this.pos.barcode_reader) {
            this.pos.barcode_reader.restore_callbacks();
        }
    },

    // hides the popup. keep in mind that this is called in 
    // the initialization pass of the pos instantiation, 
    // so you don't want to do anything fancy in here
    hide: function(){
        if (this.$el) {
            this.$el.addClass('oe_hidden');
        }
    },

    // what happens when we click cancel
    // ( it should close the popup and do nothing )
    click_cancel: function(){
        this.gui.close_popup();
        if (this.options.cancel) {
            this.options.cancel.call(this);
        }
    },

    // what happens when we confirm the action
    click_confirm: function(){
        this.gui.close_popup();
        if (this.options.confirm) {
            this.options.confirm.call(this);
        }
    },

    // Since Widget does not support extending the events declaration
    // we declared them all in the top class.
    click_item: function(){},
    click_numad: function(){},
});
gui.define_popup({name:'alert', widget: PopupWidget});

var ErrorPopupWidget = PopupWidget.extend({
    template:'ErrorPopupWidget',
    show: function(options){
        this._super(options);
        this.gui.play_sound('error');
    },
});
gui.define_popup({name:'error', widget: ErrorPopupWidget});


var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({
    template:'ErrorTracebackPopupWidget',
    show: function(opts) {
        var self = this;
        this._super(opts);

        this.$('.download').off('click').click(function(){
            self.gui.download_file(self.options.body,'traceback.txt');
        });

        this.$('.email').off('click').click(function(){
            self.gui.send_email( self.pos.company.email,
                _t('IMPORTANT: Bug Report From Odoo Point Of Sale'),
                self.options.body);
        });
    }
});
gui.define_popup({name:'error-traceback', widget: ErrorTracebackPopupWidget});


var ErrorBarcodePopupWidget = ErrorPopupWidget.extend({
    template:'ErrorBarcodePopupWidget',
    show: function(barcode){
        this._super({barcode: barcode});
    },
});
gui.define_popup({name:'error-barcode', widget: ErrorBarcodePopupWidget});


var ConfirmPopupWidget = PopupWidget.extend({
    template: 'ConfirmPopupWidget',
});
gui.define_popup({name:'confirm', widget: ConfirmPopupWidget});

/**
 * A popup that allows the user to select one item from a list. 
 *
 * show_popup('selection',{
 *      title: "Popup Title",
 *      list: [
 *          { label: 'foobar',  item: 45 },
 *          { label: 'bar foo', item: 'stuff' },
 *      ],
 *      confirm: function(item) {
 *          // get the item selected by the user.
 *      },
 *      cancel: function(){
 *          // user chose nothing
 *      }
 *  });
 */

var SelectionPopupWidget = PopupWidget.extend({
    template: 'SelectionPopupWidget',
    show: function(options){
        options = options || {};
        var self = this;
        this._super(options);

        this.list    = options.list    || [];
        this.renderElement();
    },
    click_item : function(event) {
        this.gui.close_popup();
        if (this.options.confirm) {
            var item = this.list[parseInt($(event.target).data('item-index'))];
            item = item ? item.item : item;
            this.options.confirm.call(self,item);
        }
    }
});
gui.define_popup({name:'selection', widget: SelectionPopupWidget});


var TextInputPopupWidget = PopupWidget.extend({
    template: 'TextInputPopupWidget',
    show: function(options){
        options = options || {};
        this._super(options);

        this.renderElement();
        this.$('input,textarea').focus();
    },
    click_confirm: function(){
        var value = this.$('input,textarea').val();
        this.gui.close_popup();
        if( this.options.confirm ){
            this.options.confirm.call(this,value);
        }
    },
});
gui.define_popup({name:'textinput', widget: TextInputPopupWidget});


var TextAreaPopupWidget = TextInputPopupWidget.extend({
    template: 'TextAreaPopupWidget',
});
gui.define_popup({name:'textarea', widget: TextAreaPopupWidget});

var PackLotLinePopupWidget = PopupWidget.extend({
    template: 'PackLotLinePopupWidget',
    events: _.extend({}, PopupWidget.prototype.events, {
        'click .remove-lot': 'remove_lot',
        'keydown': 'add_lot',
        'blur .packlot-line-input': 'lose_input_focus'
    }),

    show: function(options){
        this._super(options);
        this.focus();
    },

    click_confirm: function(){
        var pack_lot_lines = this.options.pack_lot_lines;
        this.$('.packlot-line-input').each(function(index, el){
            var cid = $(el).attr('cid'),
                lot_name = $(el).val();
            var pack_line = pack_lot_lines.get({cid: cid});
            pack_line.set_lot_name(lot_name);
        });
        pack_lot_lines.remove_empty_model();
        pack_lot_lines.set_quantity_by_lot();
        this.options.order.save_to_db();
        this.gui.close_popup();
    },

    add_lot: function(ev) {
        if (ev.keyCode === $.ui.keyCode.ENTER){
            var pack_lot_lines = this.options.pack_lot_lines,
                $input = $(ev.target),
                cid = $input.attr('cid'),
                lot_name = $input.val();

            var lot_model = pack_lot_lines.get({cid: cid});
            lot_model.set_lot_name(lot_name);  // First set current model then add new one
            if(!pack_lot_lines.get_empty_model()){
                var new_lot_model = lot_model.add();
                this.focus_model = new_lot_model;
            }
            pack_lot_lines.set_quantity_by_lot();
            this.renderElement();
            this.focus();
        }
    },

    remove_lot: function(ev){
        var pack_lot_lines = this.options.pack_lot_lines,
            $input = $(ev.target).prev(),
            cid = $input.attr('cid');
        var lot_model = pack_lot_lines.get({cid: cid});
        lot_model.remove();
        pack_lot_lines.set_quantity_by_lot();
        this.renderElement();
    },

    lose_input_focus: function(ev){
        var $input = $(ev.target),
            cid = $input.attr('cid');
        var lot_model = this.options.pack_lot_lines.get({cid: cid});
        lot_model.set_lot_name($input.val());
    },

    focus: function(){
        this.$("input[autofocus]").focus();
        this.focus_model = false;   // after focus clear focus_model on widget
    }
});
gui.define_popup({name:'packlotline', widget:PackLotLinePopupWidget});

var NumberPopupWidget = PopupWidget.extend({
    template: 'NumberPopupWidget',
    show: function(options){
        options = options || {};
        this._super(options);

        this.inputbuffer = '' + (options.value   || '');
        this.decimal_separator = _t.database.parameters.decimal_point;
        this.renderElement();
        this.firstinput = true;
    },
    click_numpad: function(event){
        var newbuf = this.gui.numpad_input(
            this.inputbuffer, 
            $(event.target).data('action'), 
            {'firstinput': this.firstinput});

        this.firstinput = (newbuf.length === 0);
        
        if (newbuf !== this.inputbuffer) {
            this.inputbuffer = newbuf;
            this.$('.value').text(this.inputbuffer);
        }
    },
    click_confirm: function(){
        this.gui.close_popup();
        if( this.options.confirm ){
            this.options.confirm.call(this,this.inputbuffer);
        }
    },
});
gui.define_popup({name:'number', widget: NumberPopupWidget});

var PasswordPopupWidget = NumberPopupWidget.extend({
    renderElement: function(){
        this._super();
        this.$('.popup').addClass('popup-password');
    },
});
gui.define_popup({name:'password', widget: PasswordPopupWidget});

var OrderImportPopupWidget = PopupWidget.extend({
    template: 'OrderImportPopupWidget',
});
gui.define_popup({name:'orderimport', widget: OrderImportPopupWidget});

return PopupWidget;
});
Exemplo n.º 3
0
odoo.define('pos_restaurant.floors', function (require) {
"use strict";

var PosBaseWidget = require('point_of_sale.BaseWidget');
var chrome = require('point_of_sale.chrome');
var gui = require('point_of_sale.gui');
var models = require('point_of_sale.models');
var screens = require('point_of_sale.screens');
var core = require('web.core');
var rpc = require('web.rpc');

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

// At POS Startup, load the floors, and add them to the pos model
models.load_models({
    model: 'restaurant.floor',
    fields: ['name','background_color','table_ids','sequence'],
    domain: function(self){ return [['pos_config_id','=',self.config.id]]; },
    loaded: function(self,floors){
        self.floors = floors;
        self.floors_by_id = {};
        for (var i = 0; i < floors.length; i++) {
            floors[i].tables = [];
            self.floors_by_id[floors[i].id] = floors[i];
        }

        // Make sure they display in the correct order
        self.floors = self.floors.sort(function(a,b){ return a.sequence - b.sequence; });

        // Ignore floorplan features if no floor specified.
        self.config.iface_floorplan = !!self.floors.length;
    },
});

// At POS Startup, after the floors are loaded, load the tables, and associate
// them with their floor.
models.load_models({
    model: 'restaurant.table',
    fields: ['name','width','height','position_h','position_v','shape','floor_id','color','seats'],
    loaded: function(self,tables){
        self.tables_by_id = {};
        for (var i = 0; i < tables.length; i++) {
            self.tables_by_id[tables[i].id] = tables[i];
            var floor = self.floors_by_id[tables[i].floor_id[0]];
            if (floor) {
                floor.tables.push(tables[i]);
                tables[i].floor = floor;
            }
        }
    },
});

// The Table GUI element, should always be a child of the FloorScreenWidget
var TableWidget = PosBaseWidget.extend({
    template: 'TableWidget',
    init: function(parent, options){
        this._super(parent, options);
        this.table    = options.table;
        this.selected = false;
        this.moved    = false;
        this.dragpos  = {x:0, y:0};
        this.handle_dragging = false;
        this.handle   = null;
    },
    // computes the absolute position of a DOM mouse event, used
    // when resizing tables
    event_position: function(event){
        if(event.touches && event.touches[0]){
            return {x: event.touches[0].screenX, y: event.touches[0].screenY};
        }else{
            return {x: event.screenX, y: event.screenY};
        }
    },
    // when a table is clicked, go to the table's orders
    // but if we're editing, we select/deselect it.
    click_handler: function(){
        var self = this;
        var floorplan = this.getParent();
        if (floorplan.editing) {
            setTimeout(function(){  // in a setTimeout to debounce with drag&drop start
                if (!self.dragging) {
                    if (self.moved) {
                        self.moved = false;
                    } else if (!self.selected) {
                        self.getParent().select_table(self);
                    } else {
                        self.getParent().deselect_tables();
                    }
                }
            },50);
        } else {
            floorplan.pos.set_table(this.table);
        }
    },
    // drag and drop for moving the table, at drag start
    dragstart_handler: function(event,$el,drag){
        if (this.selected && !this.handle_dragging) {
            this.dragging = true;
            this.dragpos  = { x: drag.offsetX, y: drag.offsetY };
        }
    },
    // drag and drop for moving the table, at drag end
    dragend_handler:   function(){
        this.dragging = false;
    },
    // drag and drop for moving the table, at every drop movement.
    dragmove_handler: function(event,$el,drag){
        if (this.dragging) {
            var dx   = drag.offsetX - this.dragpos.x;
            var dy   = drag.offsetY - this.dragpos.y;

            this.dragpos = { x: drag.offsetX, y: drag.offsetY };
            this.moved   = true;

            this.table.position_v += dy;
            this.table.position_h += dx;

            $el.css(this.table_style());
        }
    },
    // drag and dropping the resizing handles
    handle_dragstart_handler: function(event, $el, drag) {
        if (this.selected && !this.dragging) {
            this.handle_dragging = true;
            this.handle_dragpos  = this.event_position(event);
            this.handle          = drag.target;
        }
    },
    handle_dragend_handler: function() {
        this.handle_dragging = false;
    },
    handle_dragmove_handler: function(event) {
        if (this.handle_dragging) {
            var pos  = this.event_position(event);
            var dx   = pos.x - this.handle_dragpos.x;
            var dy   = pos.y - this.handle_dragpos.y;

            this.handle_dragpos = pos;
            this.moved   = true;

            var cl     = this.handle.classList;

            var MIN_SIZE = 40;  // smaller than this, and it becomes impossible to edit.

            var tw = Math.max(MIN_SIZE, this.table.width);
            var th = Math.max(MIN_SIZE, this.table.height);
            var tx = this.table.position_h;
            var ty = this.table.position_v;

            if (cl.contains('left') && tw - dx >= MIN_SIZE) {
                tw -= dx;
                tx += dx;
            } else if (cl.contains('right') && tw + dx >= MIN_SIZE) {
                tw += dx;
            }

            if (cl.contains('top') && th - dy >= MIN_SIZE) {
                th -= dy;
                ty += dy;
            } else if (cl.contains('bottom') && th + dy >= MIN_SIZE) {
                th += dy;
            }

            this.table.width  = tw;
            this.table.height = th;
            this.table.position_h = tx;
            this.table.position_v = ty;

            this.$el.css(this.table_style());
        }
    },
    set_table_color: function(color){
        this.table.color = _.escape(color);
        this.$el.css({'background': this.table.color});
    },
    set_table_name: function(name){
        if (name) {
            this.table.name = name;
            this.renderElement();
        }
    },
    set_table_seats: function(seats){
        if (seats) {
            this.table.seats = Number(seats);
            this.renderElement();
        }
    },
    // The table's positioning is handled via css absolute positioning,
    // which is handled here.
    table_style: function(){
        var table = this.table;
        function unit(val){ return '' + val + 'px'; }
        var style = {
            'width':        unit(table.width),
            'height':       unit(table.height),
            'line-height':  unit(table.height),
            'margin-left':  unit(-table.width/2),
            'margin-top':   unit(-table.height/2),
            'top':          unit(table.position_v + table.height/2),
            'left':         unit(table.position_h + table.width/2),
            'border-radius': table.shape === 'round' ?
                    unit(Math.max(table.width,table.height)/2) : '3px',
        };
        if (table.color) {
            style.background = table.color;
        }
        if (table.height >= 150 && table.width >= 150) {
            style['font-size'] = '32px';
        }

        return style;
    },
    // convert the style dictionary to a ; separated string for inclusion in templates
    table_style_str: function(){
        var style = this.table_style();
        var str = "";
        var s;
        for (s in style) {
            str += s + ":" + style[s] + "; ";
        }
        return str;
    },
    // select the table (should be called via the floorplan)
    select: function() {
        this.selected = true;
        this.renderElement();
    },
    // deselect the table (should be called via the floorplan)
    deselect: function() {
        this.selected = false;
        this.renderElement();
        this.save_changes();
    },
    // sends the table's modification to the server
    save_changes: function(){
        var self   = this;
        var fields = _.find(this.pos.models,function(model){ return model.model === 'restaurant.table'; }).fields;

        // we need a serializable copy of the table, containing only the fields defined on the server
        var serializable_table = {};
        for (var i = 0; i < fields.length; i++) {
            if (typeof this.table[fields[i]] !== 'undefined') {
                serializable_table[fields[i]] = this.table[fields[i]];
            }
        }
        // and the id ...
        serializable_table.id = this.table.id;

        rpc.query({
                model: 'restaurant.table',
                method: 'create_from_ui',
                args: [serializable_table],
            })
            .then(function (table_id){
                rpc.query({
                        model: 'restaurant.table',
                        method: 'search_read',
                        args: [[['id', '=', table_id]], fields],
                        limit: 1,
                    })
                    .then(function (table){
                        for (var field in table) {
                            self.table[field] = table[field];
                        }
                        self.renderElement();
                    });
            }, function(err,event) {
                self.gui.show_popup('error',{
                    'title':_t('Changes could not be saved'),
                    'body': _t('You must be connected to the internet to save your changes.'),
                });
                event.stopPropagation();
                event.preventDefault();
            });
    },
    // destroy the table.  We do not really destroy it, we set it
    // to inactive so that it doesn't show up anymore, but it still
    // available on the database for the orders that depend on it.
    trash: function(){
        var self  = this;
        rpc.query({
                model: 'restaurant.table',
                method: 'create_from_ui',
                args: [{'active':false,'id':this.table.id}],
            })
            .then(function (table_id){
                // Removing all references from the table and the table_widget in in the UI ...
                for (var i = 0; i < self.pos.floors.length; i++) {
                    var floor = self.pos.floors[i];
                    for (var j = 0; j < floor.tables.length; j++) {
                        if (floor.tables[j].id === table_id) {
                            floor.tables.splice(j,1);
                            break;
                        }
                    }
                }
                var floorplan = self.getParent();
                for (var i = 0; i < floorplan.table_widgets.length; i++) {
                    if (floorplan.table_widgets[i] === self) {
                        floorplan.table_widgets.splice(i,1);
                    }
                }
                if (floorplan.selected_table === self) {
                    floorplan.selected_table = null;
                }
                floorplan.update_toolbar();
                self.destroy();
            }, function(err, event) {
                self.gui.show_popup('error', {
                    'title':_t('Changes could not be saved'),
                    'body': _t('You must be connected to the internet to save your changes.'),
                });
                event.stopPropagation();
                event.preventDefault();
            });
    },
    get_notifications: function(){  //FIXME : Make this faster
        var orders = this.pos.get_table_orders(this.table);
        var notifications = {};
        for (var i = 0; i < orders.length; i++) {
            if (orders[i].hasChangesToPrint()) {
                notifications.printing = true;
                break;
            } else if (orders[i].hasSkippedChanges()) {
                notifications.skipped  = true;
            }
        }
        return notifications;
    },
        update_click_handlers: function(editing){
            var self = this;
            this.$el.off('mouseup touchend touchcancel click dragend');

            if (editing) {
                this.$el.on('mouseup touchend touchcancel', function(event){ self.click_handler(event,$(this)); });
            } else {
                this.$el.on('click dragend', function(event){ self.click_handler(event,$(this)); });
            }
        },
    renderElement: function(){
        var self = this;
        this.order_count    = this.pos.get_table_orders(this.table).length;
        this.customer_count = this.pos.get_customer_count(this.table);
        this.fill           = Math.min(1,Math.max(0,this.customer_count / this.table.seats));
        this.notifications  = this.get_notifications();
        this._super();

        this.update_click_handlers();

        this.$el.on('dragstart', function(event,drag){ self.dragstart_handler(event,$(this),drag); });
        this.$el.on('drag',      function(event,drag){ self.dragmove_handler(event,$(this),drag); });
        this.$el.on('dragend',   function(event,drag){ self.dragend_handler(event,$(this),drag); });

        var handles = this.$el.find('.table-handle');
        handles.on('dragstart',  function(event,drag){ self.handle_dragstart_handler(event,$(this),drag); });
        handles.on('drag',       function(event,drag){ self.handle_dragmove_handler(event,$(this),drag); });
        handles.on('dragend',    function(event,drag){ self.handle_dragend_handler(event,$(this),drag); });
    },
});

// The screen that allows you to select the floor, see and select the table,
// as well as edit them.
var FloorScreenWidget = screens.ScreenWidget.extend({
    template: 'FloorScreenWidget',

    // Ignore products, discounts, and client barcodes
    barcode_product_action: function(code){},
    barcode_discount_action: function(code){},
    barcode_client_action: function(code){},

    init: function(parent, options) {
        this._super(parent, options);
        this.floor = this.pos.floors[0];
        this.table_widgets = [];
        this.selected_table = null;
        this.editing = false;
    },
    hide: function(){
        this._super();
        if (this.editing) {
            this.toggle_editing();
        }
        this.chrome.widget.order_selector.show();
    },
    show: function(){
        this._super();
        this.chrome.widget.order_selector.hide();
        for (var i = 0; i < this.table_widgets.length; i++) {
            this.table_widgets[i].renderElement();
        }
        this.check_empty_floor();
    },
    click_floor_button: function(event,$el){
        var floor = this.pos.floors_by_id[$el.data('id')];
        if (floor !== this.floor) {
            if (this.editing) {
                this.toggle_editing();
            }
            this.floor = floor;
            this.selected_table = null;
            this.renderElement();
            this.check_empty_floor();
        }
    },
    background_image_url: function(floor) {
        return '/web/image?model=restaurant.floor&id='+floor.id+'&field=background_image';
    },
    get_floor_style: function() {
        var style = "";
        if (this.floor.background_image) {
            style += "background-image: url(" + this.background_image_url(this.floor) + "); ";
        }
        if (this.floor.background_color) {
            style += "background-color: " + _.escape(this.floor.background_color) + ";";
        }
        return style;
    },
    set_background_color: function(background) {
        var self = this;
        this.floor.background_color = background;
        rpc.query({
                model: 'restaurant.floor',
                method: 'write',
                args: [[this.floor.id], {'background_color': background}],
            })
            .fail(function (err, event){
                self.gui.show_popup('error',{
                    'title':_t('Changes could not be saved'),
                    'body': _t('You must be connected to the internet to save your changes.'),
                });
                event.stopPropagation();
                event.preventDefault();
            });
        this.$('.floor-map').css({"background-color": _.escape(background)});
    },
    deselect_tables: function(){
        for (var i = 0; i < this.table_widgets.length; i++) {
            var table = this.table_widgets[i];
            if (table.selected) {
                table.deselect();
            }
        }
        this.selected_table = null;
        this.update_toolbar();
    },
    select_table: function(table_widget){
        if (!table_widget.selected) {
            this.deselect_tables();
            table_widget.select();
            this.selected_table = table_widget;
            this.update_toolbar();
        }
    },
    tool_shape_action: function(){
        if (this.selected_table) {
            var table = this.selected_table.table;
            if (table.shape === 'square') {
                table.shape = 'round';
            } else {
                table.shape = 'square';
            }
            this.selected_table.renderElement();
            this.update_toolbar();
        }
    },
    tool_colorpicker_open: function(){
        this.$('.color-picker').addClass('oe_hidden');
        if (this.selected_table) {
            this.$('.color-picker.fg-picker').removeClass('oe_hidden');
        } else {
            this.$('.color-picker.bg-picker').removeClass('oe_hidden');
        }
    },
    tool_colorpicker_pick: function(event,$el){
        if (this.selected_table) {
            this.selected_table.set_table_color($el[0].style['background-color']);
        } else {
            this.set_background_color($el[0].style['background-color']);
        }
    },
    tool_colorpicker_close: function(){
        this.$('.color-picker').addClass('oe_hidden');
    },
    tool_rename_table: function(){
        var self = this;
        if (this.selected_table) {
            this.gui.show_popup('textinput',{
                'title':_t('Table Name ?'),
                'value': this.selected_table.table.name,
                'confirm': function(value) {
                    self.selected_table.set_table_name(value);
                },
            });
        }
    },
    tool_change_seats: function(){
        var self = this;
        if (this.selected_table) {
            this.gui.show_popup('number',{
                'title':_t('Number of Seats ?'),
                'cheap': true,
                'value': this.selected_table.table.seats,
                'confirm': function(value) {
                    self.selected_table.set_table_seats(value);
                },
            });
        }
    },
    tool_duplicate_table: function(){
        if (this.selected_table) {
            var tw = this.create_table(this.selected_table.table);
            tw.table.position_h += 10;
            tw.table.position_v += 10;
            tw.save_changes();
            this.select_table(tw);
        }
    },
    tool_new_table: function(){
        var tw = this.create_table({
            'position_v': 100,
            'position_h': 100,
            'width': 75,
            'height': 75,
            'shape': 'square',
            'seats': 1,
        });
        tw.save_changes();
        this.select_table(tw);
        this.check_empty_floor();
    },
    new_table_name: function(name){
        if (name) {
            var num = Number((name.match(/\d+/g) || [])[0] || 0);
            var str = (name.replace(/\d+/g,''));
            var n   = {num: num, str:str};
                n.num += 1;
            this.last_name = n;
        } else if (this.last_name) {
            this.last_name.num += 1;
        } else {
            this.last_name = {num: 1, str:'T'};
        }
        return '' + this.last_name.str + this.last_name.num;
    },
    create_table: function(params) {
        var table = {};
        for (var p in params) {
            table[p] = params[p];
        }

        table.name = this.new_table_name(params.name);

        delete table.id;
        table.floor_id = [this.floor.id,''];
        table.floor = this.floor;

        this.floor.tables.push(table);
        var tw = new TableWidget(this,{table: table});
            tw.appendTo('.floor-map .tables');
        this.table_widgets.push(tw);
        return tw;
    },
    tool_trash_table: function(){
        var self = this;
        if (this.selected_table) {
            this.gui.show_popup('confirm',{
                'title':  _t('Are you sure ?'),
                'comment':_t('Removing a table cannot be undone'),
                'confirm': function(){
                    self.selected_table.trash();
                },
            });
        }
    },
    toggle_editing: function(){
        this.editing = !this.editing;
        this.update_toolbar();
            this.update_table_click_handlers();

        if (!this.editing) {
            this.deselect_tables();
            }
        },
        update_table_click_handlers: function(){
            for (var i = 0; i < this.table_widgets.length; ++i) {
                if (this.editing) {
                    this.table_widgets[i].update_click_handlers("editing");
                } else {
                    this.table_widgets[i].update_click_handlers();
                }
        }
    },
    check_empty_floor: function(){
        if (!this.floor.tables.length) {
            if (!this.editing) {
                this.toggle_editing();
            }
            this.$('.empty-floor').removeClass('oe_hidden');
        } else {
            this.$('.empty-floor').addClass('oe_hidden');
        }
    },
    update_toolbar: function(){

        if (this.editing) {
            this.$('.edit-bar').removeClass('oe_hidden');
            this.$('.edit-button.editing').addClass('active');
        } else {
            this.$('.edit-bar').addClass('oe_hidden');
            this.$('.edit-button.editing').removeClass('active');
        }

        if (this.selected_table) {
            this.$('.needs-selection').removeClass('disabled');
            var table = this.selected_table.table;
            if (table.shape === 'square') {
                this.$('.button-option.square').addClass('oe_hidden');
                this.$('.button-option.round').removeClass('oe_hidden');
            } else {
                this.$('.button-option.square').removeClass('oe_hidden');
                this.$('.button-option.round').addClass('oe_hidden');
            }
        } else {
            this.$('.needs-selection').addClass('disabled');
        }
        this.tool_colorpicker_close();
    },
    renderElement: function(){
        var self = this;

        // cleanup table widgets from previous renders
        for (var i = 0; i < this.table_widgets.length; i++) {
            this.table_widgets[i].destroy();
        }

        this.table_widgets = [];

        this._super();

        for (var i = 0; i < this.floor.tables.length; i++) {
            var tw = new TableWidget(this,{
                table: this.floor.tables[i],
            });
            tw.appendTo(this.$('.floor-map .tables'));
            this.table_widgets.push(tw);
        }

        this.$('.floor-selector .button').click(function(event){
            self.click_floor_button(event,$(this));
        });

        this.$('.edit-button.shape').click(function(){
            self.tool_shape_action();
        });

        this.$('.edit-button.color').click(function(){
            self.tool_colorpicker_open();
        });

        this.$('.edit-button.dup-table').click(function(){
            self.tool_duplicate_table();
        });

        this.$('.edit-button.new-table').click(function(){
            self.tool_new_table();
        });

        this.$('.edit-button.rename').click(function(){
            self.tool_rename_table();
        });

        this.$('.edit-button.seats').click(function(){
            self.tool_change_seats();
        });

        this.$('.edit-button.trash').click(function(){
            self.tool_trash_table();
        });

        this.$('.color-picker .close-picker').click(function(event){
            self.tool_colorpicker_close();
            event.stopPropagation();
        });

        this.$('.color-picker .color').click(function(event){
            self.tool_colorpicker_pick(event,$(this));
            event.stopPropagation();
        });

        this.$('.edit-button.editing').click(function(){
            self.toggle_editing();
        });

        this.$('.floor-map,.floor-map .tables').click(function(event){
            if (event.target === self.$('.floor-map')[0] ||
                event.target === self.$('.floor-map .tables')[0]) {
                self.deselect_tables();
            }
        });

        this.$('.color-picker .close-picker').click(function(event){
            self.tool_colorpicker_close();
            event.stopPropagation();
        });

        this.update_toolbar();

    },
});

gui.define_screen({
    'name': 'floors',
    'widget': FloorScreenWidget,
    'condition': function(){
        return this.pos.config.iface_floorplan;
    },
});

// Add the FloorScreen to the GUI, and set it as the default screen
chrome.Chrome.include({
    build_widgets: function(){
        this._super();
        if (this.pos.config.iface_floorplan) {
            this.gui.set_startup_screen('floors');
        }
    },
});

// New orders are now associated with the current table, if any.
var _super_order = models.Order.prototype;
models.Order = models.Order.extend({
    initialize: function() {
        _super_order.initialize.apply(this,arguments);
        if (!this.table) {
            this.table = this.pos.table;
        }
        this.customer_count = this.customer_count || 1;
        this.save_to_db();
    },
    export_as_JSON: function() {
        var json = _super_order.export_as_JSON.apply(this,arguments);
        json.table     = this.table ? this.table.name : undefined;
        json.table_id  = this.table ? this.table.id : false;
        json.floor     = this.table ? this.table.floor.name : false;
        json.floor_id  = this.table ? this.table.floor.id : false;
        json.customer_count = this.customer_count;
        return json;
    },
    init_from_JSON: function(json) {
        _super_order.init_from_JSON.apply(this,arguments);
        this.table = this.pos.tables_by_id[json.table_id];
        this.floor = this.table ? this.pos.floors_by_id[json.floor_id] : undefined;
        this.customer_count = json.customer_count || 1;
    },
    export_for_printing: function() {
        var json = _super_order.export_for_printing.apply(this,arguments);
        json.table = this.table ? this.table.name : undefined;
        json.floor = this.table ? this.table.floor.name : undefined;
        json.customer_count = this.get_customer_count();
        return json;
    },
    get_customer_count: function(){
        return this.customer_count;
    },
    set_customer_count: function(count) {
        this.customer_count = Math.max(count,0);
        this.trigger('change');
    },
});

// We need to modify the OrderSelector to hide itself when we're on
// the floor plan
chrome.OrderSelectorWidget.include({
    floor_button_click_handler: function(){
        this.pos.set_table(null);
    },
    hide: function(){
        this.$el.addClass('oe_invisible');
    },
    show: function(){
        this.$el.removeClass('oe_invisible');
    },
    renderElement: function(){
        var self = this;
        this._super();
        if (this.pos.config.iface_floorplan) {
            if (this.pos.get_order()) {
                if (this.pos.table && this.pos.table.floor) {
                    this.$('.orders').prepend(QWeb.render('BackToFloorButton',{table: this.pos.table, floor:this.pos.table.floor}));
                    this.$('.floor-button').click(function(){
                        self.floor_button_click_handler();
                    });
                }
                this.$el.removeClass('oe_invisible');
            } else {
                this.$el.addClass('oe_invisible');
            }
        }
    },
});

// We need to change the way the regular UI sees the orders, it
// needs to only see the orders associated with the current table,
// and when an order is validated, it needs to go back to the floor map.
//
// And when we change the table, we must create an order for that table
// if there is none.
var _super_posmodel = models.PosModel.prototype;
models.PosModel = models.PosModel.extend({
    initialize: function(session, attributes) {
        this.table = null;
        return _super_posmodel.initialize.call(this,session,attributes);
    },

    transfer_order_to_different_table: function () {
        this.order_to_transfer_to_different_table = this.get_order();

        // go to 'floors' screen, this will set the order to null and
        // eventually this will cause the gui to go to its
        // default_screen, which is 'floors'
        this.set_table(null);
    },

    // changes the current table.
    set_table: function(table) {
        if (!table) { // no table ? go back to the floor plan, see ScreenSelector
            this.set_order(null);
        } else if (this.order_to_transfer_to_different_table) {
            this.order_to_transfer_to_different_table.table = table;
            this.order_to_transfer_to_different_table.save_to_db();
            this.order_to_transfer_to_different_table = null;

            // set this table
            this.set_table(table);

        } else {
            this.table = table;
            var orders = this.get_order_list();
            if (orders.length) {
                this.set_order(orders[0]); // and go to the first one ...
            } else {
                this.add_new_order();  // or create a new order with the current table
            }
        }
    },

    // if we have tables, we do not load a default order, as the default order will be
    // set when the user selects a table.
    set_start_order: function() {
        if (!this.config.iface_floorplan) {
            _super_posmodel.set_start_order.apply(this,arguments);
        }
    },

    // we need to prevent the creation of orders when there is no
    // table selected.
    add_new_order: function() {
        if (this.config.iface_floorplan) {
            if (this.table) {
                _super_posmodel.add_new_order.call(this);
            } else {
                console.warn("WARNING: orders cannot be created when there is no active table in restaurant mode");
            }
        } else {
            _super_posmodel.add_new_order.apply(this,arguments);
        }
    },


    // get the list of unpaid orders (associated to the current table)
    get_order_list: function() {
        var orders = _super_posmodel.get_order_list.call(this);
        if (!this.config.iface_floorplan) {
            return orders;
        } else if (!this.table) {
            return [];
        } else {
            var t_orders = [];
            for (var i = 0; i < orders.length; i++) {
                if ( orders[i].table === this.table) {
                    t_orders.push(orders[i]);
                }
            }
            return t_orders;
        }
    },

    // get the list of orders associated to a table. FIXME: should be O(1)
    get_table_orders: function(table) {
        var orders   = _super_posmodel.get_order_list.call(this);
        var t_orders = [];
        for (var i = 0; i < orders.length; i++) {
            if (orders[i].table === table) {
                t_orders.push(orders[i]);
            }
        }
        return t_orders;
    },

    // get customer count at table
    get_customer_count: function(table) {
        var orders = this.get_table_orders(table);
        var count  = 0;
        for (var i = 0; i < orders.length; i++) {
            count += orders[i].get_customer_count();
        }
        return count;
    },

    // When we validate an order we go back to the floor plan.
    // When we cancel an order and there is multiple orders
    // on the table, stay on the table.
    on_removed_order: function(removed_order,index,reason){
        if (this.config.iface_floorplan) {
            var order_list = this.get_order_list();
            if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){
                this.set_order(order_list[index] || order_list[order_list.length -1]);
            }else{
                // back to the floor plan
                this.set_table(null);
            }
        } else {
            _super_posmodel.on_removed_order.apply(this,arguments);
        }
    },


});

var TableGuestsButton = screens.ActionButtonWidget.extend({
    template: 'TableGuestsButton',
    guests: function() {
        if (this.pos.get_order()) {
            return this.pos.get_order().customer_count;
        } else {
            return 0;
        }
    },
    button_click: function() {
        var self = this;
        this.gui.show_popup('number', {
            'title':  _t('Guests ?'),
            'cheap': true,
            'value':   this.pos.get_order().customer_count,
            'confirm': function(value) {
                value = Math.max(1,Number(value));
                self.pos.get_order().set_customer_count(value);
                self.renderElement();
            },
        });
    },
});

screens.OrderWidget.include({
    update_summary: function(){
        this._super();
        if (this.getParent().action_buttons &&
            this.getParent().action_buttons.guests) {
            this.getParent().action_buttons.guests.renderElement();
        }
    },
});

screens.define_action_button({
    'name': 'guests',
    'widget': TableGuestsButton,
    'condition': function(){
        return this.pos.config.iface_floorplan;
    },
});

var TransferOrderButton = screens.ActionButtonWidget.extend({
    template: 'TransferOrderButton',
    button_click: function() {
        this.pos.transfer_order_to_different_table();
    },
});

screens.define_action_button({
    'name': 'transfer',
    'widget': TransferOrderButton,
    'condition': function(){
        return this.pos.config.iface_floorplan;
    },
});

});
Exemplo n.º 4
0
odoo.define('point_of_sale.popups', function (require) {
"use strict";

// This file contains the Popups.
// Popups must be loaded and named in chrome.js. 
// They are instanciated / destroyed with the .gui.show_popup()
// and .gui.close_popup() methods.

var PosBaseWidget = require('point_of_sale.BaseWidget');
var gui = require('point_of_sale.gui');


var PopupWidget = PosBaseWidget.extend({
    init: function(parent, args) {
        this._super(parent, args);
        this.options = {};
    },
    events: {
        'click .button.cancel':  'click_cancel',
        'click .button.confirm': 'click_confirm',
        'click .selection-item': 'click_item',
        'click .input-button':   'click_numpad',
        'click .mode-button':    'click_numpad',
    },

    // show the popup !  
    show: function(options){
        if(this.$el){
            this.$el.removeClass('oe_hidden');
        }
        
        if (typeof options === 'string') {
            this.options = {title: options};
        } else {
            this.options = options || {};
        }

        this.renderElement();

        // popups block the barcode reader ... 
        if (this.pos.barcode_reader) {
            this.pos.barcode_reader.save_callbacks();
            this.pos.barcode_reader.reset_action_callbacks();
        }
    },

    // called before hide, when a popup is closed.
    // extend this if you want a custom action when the 
    // popup is closed.
    close: function(){
        if (this.pos.barcode_reader) {
            this.pos.barcode_reader.restore_callbacks();
        }
    },

    // hides the popup. keep in mind that this is called in 
    // the initialization pass of the pos instantiation, 
    // so you don't want to do anything fancy in here
    hide: function(){
        if (this.$el) {
            this.$el.addClass('oe_hidden');
        }
    },

    // what happens when we click cancel
    // ( it should close the popup and do nothing )
    click_cancel: function(){
        this.gui.close_popup();
        if (this.options.cancel) {
            this.options.cancel.call(this);
        }
    },

    // what happens when we confirm the action
    click_confirm: function(){
        this.gui.close_popup();
        if (this.options.confirm) {
            this.options.confirm.call(this);
        }
    },

    // Since Widget does not support extending the events declaration
    // we declared them all in the top class.
    click_item: function(){},
    click_numad: function(){},
});

var ErrorPopupWidget = PopupWidget.extend({
    template:'ErrorPopupWidget',
    show: function(options){
        this._super(options);
        this.gui.play_sound('error');
    },
});
gui.define_popup({name:'error', widget: ErrorPopupWidget});


var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({
    template:'ErrorTracebackPopupWidget',
});
gui.define_popup({name:'error-traceback', widget: ErrorTracebackPopupWidget});


var ErrorBarcodePopupWidget = ErrorPopupWidget.extend({
    template:'ErrorBarcodePopupWidget',
    show: function(barcode){
        this._super({barcode: barcode});
    },
});
gui.define_popup({name:'error-barcode', widget: ErrorBarcodePopupWidget});


var ConfirmPopupWidget = PopupWidget.extend({
    template: 'ConfirmPopupWidget',
});
gui.define_popup({name:'confirm', widget: ConfirmPopupWidget});

/**
 * A popup that allows the user to select one item from a list. 
 *
 * show_popup('selection',{
 *      title: "Popup Title",
 *      list: [
 *          { label: 'foobar',  item: 45 },
 *          { label: 'bar foo', item: 'stuff' },
 *      ],
 *      confirm: function(item) {
 *          // get the item selected by the user.
 *      },
 *      cancel: function(){
 *          // user chose nothing
 *      }
 *  });
 */

var SelectionPopupWidget = PopupWidget.extend({
    template: 'SelectionPopupWidget',
    show: function(options){
        options = options || {};
        var self = this;
        this._super(options);

        this.list    = options.list    || [];
        this.renderElement();

        this.$('.selection-item').click(function(){
            if (options.confirm) {
                var item = self.list[parseInt($(this).data('item-index'))];
                item = item ? item.item : item;
                options.confirm.call(self,item);
            }
        });
    },
    click_item : function(event) {
        this.gui.close_popup();
        if (this.options.confirm) {
            var item = this.list[parseInt($(event.target).data('item-index'))];
            item = item ? item.item : item;
        }
    }
});
gui.define_popup({name:'selection', widget: SelectionPopupWidget});


var TextInputPopupWidget = PopupWidget.extend({
    template: 'TextInputPopupWidget',
    show: function(options){
        options = options || {};
        this._super(options);

        this.renderElement();
        this.$('input,textarea').focus();
    },
    click_confirm: function(){
        var value = this.$('input,textarea').val();
        this.gui.close_popup();
        if( this.options.confirm ){
            this.options.confirm.call(this,value);
        }
    },
});
gui.define_popup({name:'textinput', widget: TextInputPopupWidget});


var TextAreaPopupWidget = TextInputPopupWidget.extend({
    template: 'TextAreaPopupWidget',
});
gui.define_popup({name:'textarea', widget: TextAreaPopupWidget});


var NumberPopupWidget = PopupWidget.extend({
    template: 'NumberPopupWidget',
    show: function(options){
        options = options || {};
        this._super(options);

        this.inputbuffer = '' + options.value   || '';
        this.renderElement();
        this.firstinput = true;
    },
    click_numpad: function(event){
        var newbuf = this.gui.numpad_input(
            this.inputbuffer, 
            $(event.target).data('action'), 
            {'firstinput': this.firstinput});

        this.firstinput = (newbuf.length === 0);
        
        if (newbuf !== this.inputbuffer) {
            this.inputbuffer = newbuf;
            this.$('.value').text(this.inputbuffer);
        }
    },
    click_confirm: function(){
        this.gui.close_popup();
        if( this.options.confirm ){
            this.options.confirm.call(this,this.inputbuffer);
        }
    },
});
gui.define_popup({name:'number', widget: NumberPopupWidget});

var PasswordPopupWidget = NumberPopupWidget.extend({
    renderElement: function(){
        this._super();
        this.$('.popup').addClass('popup-password');
    },
});
gui.define_popup({name:'password', widget: PasswordPopupWidget});

var UnsentOrdersPopupWidget = ConfirmPopupWidget.extend({
    template: 'UnsentOrdersPopupWidget',
});
gui.define_popup({name:'unsent-orders', widget: UnsentOrdersPopupWidget});

var UnpaidOrdersPopupWidget = ConfirmPopupWidget.extend({
    template: 'UnpaidOrdersPopupWidget',
});
gui.define_popup({name:'unpaid-orders', widget: UnpaidOrdersPopupWidget});

});