function openerp_pos_devices(instance,module){ //module is instance.point_of_sale // 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. module.ProxyDevice = instance.web.Class.extend({ init: function(options){ options = options || {}; url = options.url || 'http://localhost:8069'; this.weight = 0; this.weighting = 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.connection = new instance.web.JsonRPC(); this.connection.setup(url); this.connection.session_id = _.uniqueId('posproxy'); this.bypass_proxy = false; this.notifications = {}; }, message : function(name,params){ var ret = new $.Deferred(); var callbacks = this.notifications[name] || []; for(var i = 0; i < callbacks.length; i++){ callbacks[i](params); } this.connection.rpc('/pos/' + name, params || {}).done(function(result) { ret.resolve(result); }).fail(function(error) { ret.reject(error); }); return ret; }, // 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); }, //a product has been scanned and recognized with success // ean is a parsed ean object scan_item_success: function(ean){ return this.message('scan_item_success',{ean: ean}); }, // a product has been scanned but not recognized // ean is a parsed ean object scan_item_error_unrecognized: function(ean){ return this.message('scan_item_error_unrecognized',{ean: ean}); }, //the client is asking for help help_needed: function(){ return this.message('help_needed'); }, //the client does not need help anymore help_canceled: function(){ return this.message('help_canceled'); }, //the client is starting to weight weighting_start: function(){ if(!this.weighting){ this.weighting = true; if(!this.bypass_proxy){ this.weight = 0; return this.message('weighting_start'); } } }, //returns the weight on the scale. // is called at regular interval (up to 10x/sec) between a weighting_start() // and a weighting_end() weighting_read_kg: function(){ var self = this; this.message('weighting_read_kg',{}) .done(function(weight){ if(self.weighting){ if(self.use_debug_weight){ self.weight = self.debug_weight; }else{ self.weight = weight; } } }); return this.weight; }, // 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; }, // the client has finished weighting products weighting_end: function(){ this.weight = 0; this.weighting = false; this.message('weighting_end'); }, // the pos asks the client to pay 'price' units payment_request: function(price){ var ret = new $.Deferred(); this.paying = true; this.custom_payment_status = this.default_payment_status; return this.message('payment_request',{'price':price}); }, payment_status: function(){ if(this.bypass_proxy){ this.bypass_proxy = false; return (new $.Deferred()).resolve(this.custom_payment_status); }else{ return this.message('payment_status'); } }, // override what the proxy says and accept the payment debug_accept_payment: function(){ this.bypass_proxy = true; this.custom_payment_status = { status: 'paid', message: 'Successfull Payment, have a nice day', payment_method: 'AMEX', receipt_client: 'bla', receipt_shop: 'bla', }; }, // override what the proxy says and reject the payment debug_reject_payment: function(){ this.bypass_proxy = true; this.custom_payment_status = { status: 'error-rejected', message: 'Sorry you don\'t have enough money :(', }; }, // the client cancels his payment payment_cancel: function(){ this.paying = false; this.custom_payment_status = 'waiting_for_payment'; return this.message('payment_cancel'); }, // called when the client logs in or starts to scan product transaction_start: function(){ return this.message('transaction_start'); }, // called when the clients has finished his interaction with the machine transaction_end: function(){ return this.message('transaction_end'); }, // called when the POS turns to cashier mode cashier_mode_activated: function(){ return this.message('cashier_mode_activated'); }, // called when the POS turns to client mode cashier_mode_deactivated: function(){ return this.message('cashier_mode_deactivated'); }, // 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 * receipt is a JSON object with the following specs: * receipt{ * - orderlines : list of orderlines : * { * quantity: (number) the number of items, or the weight, * unit_name: (string) the name of the item's unit (kg, dozen, ...) * price: (number) the price of one unit of the item before discount * discount: (number) the discount on the product in % [0,100] * product_name: (string) the name of the product * price_with_tax: (number) the price paid for this orderline, tax included * price_without_tax: (number) the price paid for this orderline, without taxes * tax: (number) the price paid in taxes on this orderline * product_description: (string) generic description of the product * product_description_sale: (string) sales related information of the product * } * - paymentlines : list of paymentlines : * { * amount: (number) the amount paid * journal: (string) the name of the journal on wich the payment has been made * } * - total_with_tax: (number) the total of the receipt tax included * - total_without_tax: (number) the total of the receipt without taxes * - total_tax: (number) the total amount of taxes paid * - total_paid: (number) the total sum paid by the client * - change: (number) the amount of change given back to the client * - name: (string) a unique name for this order * - client: (string) name of the client. or null if no client is logged * - cashier: (string) the name of the cashier * - date: { the date at wich the payment has been done * year: (number) the year [2012, ...] * month: (number) the month [0,11] * date: (number) the day of the month [1,31] * day: (number) the day of the week [0,6] * hour: (number) the hour [0,23] * minute: (number) the minute [0,59] * } */ print_receipt: function(receipt){ return this.message('print_receipt',{receipt: receipt}); }, // asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server ) print_pdf_invoice: function(pdfinvoice){ return this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice}); }, }); // 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. module.BarcodeReader = instance.web.Class.extend({ actions:[ 'product', 'cashier', 'client', 'discount', ], init: function(attributes){ this.pos = attributes.pos; this.action_callback = {}; this.action_callback_stack = []; this.weight_prefix_set = attributes.weight_prefix_set || {'21':''}; this.discount_prefix_set = attributes.discount_prefix_set || {'22':''}; this.price_prefix_set = attributes.price_prefix_set || {'23':''}; this.cashier_prefix_set = attributes.cashier_prefix_set || {'041':''}; this.client_prefix_set = attributes.client_prefix_set || {'042':''}; }, save_callbacks: function(){ var callbacks = {}; for(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 an ean is scanned and parsed, the callback corresponding // to its type is called with the parsed_ean as a parameter. // (parsed_ean is the result of parse_ean(ean)) // // callbacks is a Map of 'actions' : callback(parsed_ean) // 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(action in actions){ this.set_action_callback(action,actions[action]); } } }, //remove all action callbacks reset_action_callbacks: function(){ for(action in this.action_callback){ this.action_callback[action] = undefined; } }, // returns the checksum of the ean, or -1 if the ean has not the correct length, ean must be a string ean_checksum: function(ean){ var code = ean.split(''); if(code.length !== 13){ return -1; } var oddsum = 0, evensum = 0, total = 0; code = code.reverse().splice(1); for(var i = 0; i < code.length; i++){ if(i % 2 == 0){ oddsum += Number(code[i]); }else{ evensum += Number(code[i]); } } total = oddsum * 3 + evensum; return Number((10 - total % 10) % 10); }, // returns true if the ean is a valid EAN codebar number by checking the control digit. // ean must be a string check_ean: function(ean){ return this.ean_checksum(ean) === Number(ean[ean.length-1]); }, // returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string. sanitize_ean:function(ean){ ean = ean.substr(0,13); for(var n = 0, count = (13 - ean.length); n < count; n++){ ean = ean + '0'; } return ean.substr(0,12) + this.ean_checksum(ean); }, // attempts to interpret an ean (string encoding an ean) // it will check its validity then return an object containing various // information about the ean. // most importantly : // - ean : the ean // - type : the type of the ean: // 'price' | 'weight' | 'unit' | 'cashier' | 'client' | 'discount' | 'error' // // - prefix : the prefix that has ben used to determine the type // - id : the part of the ean that identifies something // - value : if the id encodes a numerical value, it will be put there // - unit : if the encoded value has a unit, it will be put there. // not to be confused with the 'unit' type, which represent an unit of a // unique product parse_ean: function(ean){ var parse_result = { type:'unknown', // prefix:'', ean:ean, base_ean: ean, id:'', value: 0, unit: 'none', }; function match_prefix(prefix_set, type){ for(prefix in prefix_set){ if(ean.substring(0,prefix.length) === prefix){ parse_result.prefix = prefix; parse_result.type = type; return true; } } return false; } if (!this.check_ean(ean)){ parse_result.type = 'error'; } else if( match_prefix(this.price_prefix_set,'price')){ parse_result.id = ean.substring(0,7); parse_result.base_ean = this.sanitize_ean(ean.substring(0,7)); parse_result.value = Number(ean.substring(7,12))/100.0; parse_result.unit = 'euro'; } else if( match_prefix(this.weight_prefix_set,'weight')){ parse_result.id = ean.substring(0,7); parse_result.value = Number(ean.substring(7,12))/1000.0; parse_result.base_ean = this.sanitize_ean(ean.substring(0,7)); parse_result.unit = 'Kg'; } else if( match_prefix(this.client_prefix_set,'client')){ parse_result.id = ean.substring(0,7); parse_result.unit = 'Kg'; } else if( match_prefix(this.cashier_prefix_set,'cashier')){ parse_result.id = ean.substring(0,7); } else if( match_prefix(this.discount_prefix_set,'discount')){ parse_result.id = ean.substring(0,7); parse_result.base_ean = this.sanitize_ean(ean.substring(0,7)); parse_result.value = Number(ean.substring(7,12))/100.0; parse_result.unit = '%'; } else { parse_result.type = 'unit'; parse_result.prefix = ''; parse_result.id = ean; } return parse_result; }, on_ean: function(ean){ var parse_result = this.parse_ean(ean); if (parse_result.type === 'error') { //most likely a checksum error, raise warning console.warn('WARNING: barcode checksum error:',parse_result); }else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product if(this.action_callback['product']){ this.action_callback['product'](parse_result); } //this.trigger("codebar",parse_result ); }else{ if(this.action_callback[parse_result.type]){ this.action_callback[parse_result.type](parse_result); } } }, // starts catching keyboard events and tries to interpret codebar // calling the callbacks when needed. connect: function(){ var self = this; var codeNumbers = []; var timeStamp = 0; var lastTimeStamp = 0; // The barcode readers acts as a keyboard, we catch all keyup events and try to find a // barcode sequence in the typed keys, then act accordingly. this.handler = function(e){ //We only care about numbers if (e.which >= 48 && e.which < 58){ // The barcode reader sends keystrokes with a specific interval. // We look if the typed keys fit in the interval. if (codeNumbers.length === 0) { timeStamp = new Date().getTime(); } else { if (lastTimeStamp + 30 < new Date().getTime()) { // not a barcode reader codeNumbers = []; timeStamp = new Date().getTime(); } } codeNumbers.push(e.which - 48); lastTimeStamp = new Date().getTime(); if (codeNumbers.length === 13) { //We have found what seems to be a valid codebar self.on_ean(codeNumbers.join('')); codeNumbers = []; } } else { // NaN codeNumbers = []; } }; $('body').on('keypress', this.handler); }, // stops catching keyboard events disconnect: function(){ $('body').off('keypress', this.handler) }, }); }