diff --git a/addons/barcodes/barcodes.py b/addons/barcodes/barcodes.py index c9b9118728a..5a780657c1a 100644 --- a/addons/barcodes/barcodes.py +++ b/addons/barcodes/barcodes.py @@ -173,26 +173,28 @@ class barcode_nomenclature(osv.osv): return parsed_result -ENCODINGS = [ - ('any','Any'), - ('ean13','EAN-13'), - ('ean8','EAN-8'), - ('upca','UPC-A'), -] - class barcode_rule(models.Model): _name = 'barcode.rule' _order = 'sequence asc' + @api.model + def _encoding_selection_list(self): + return [ + ('any', 'Any'), + ('ean13', 'EAN-13'), + ('ean8', 'EAN-8'), + ('upca', 'UPC-A'), + ] + @api.model def _get_type_selection(self): return [('alias','Alias'),('product','Unit Product')] - + _columns = { 'name': fields.char('Rule Name', size=32, required=True, help='An internal identification for this barcode nomenclature rule'), 'barcode_nomenclature_id': fields.many2one('barcode.nomenclature','Barcode Nomenclature'), 'sequence': fields.integer('Sequence', help='Used to order rules such that rules with a smaller sequence match first'), - 'encoding': fields.selection(ENCODINGS,'Encoding',required=True,help='This rule will apply only if the barcode is encoded with the specified encoding'), + 'encoding': fields.selection('_encoding_selection_list','Encoding',required=True,help='This rule will apply only if the barcode is encoded with the specified encoding'), 'type': fields.selection('_get_type_selection','Type', required=True), 'pattern': fields.char('Barcode Pattern', size=32, help="The barcode matching pattern", required=True), 'alias': fields.char('Alias',size=32,help='The matched pattern will alias to this barcode', required=True), diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index 587d092fbad..64e30bcfbe4 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -8,8 +8,8 @@ var web_client = require('web.web_client'); var _t = core._t; // the JobQueue schedules a sequence of 'jobs'. each job is -// a function returning a deferred. the queue waits for each job to finish -// before launching the next. Each job can also be scheduled with a delay. +// a function returning a deferred. the queue waits for each job to finish +// before launching the next. Each job can also be scheduled with a delay. // the is used to prevent parallel requests to the proxy. var JobQueue = function(){ @@ -36,13 +36,13 @@ var JobQueue = function(){ // we run the job and put in def when it finishes var def = job.fun() || (new $.Deferred()).resolve(); - - // we don't care if a job fails ... + + // we don't care if a job fails ... def.always(function(){ // we run the next job after the scheduled_end_time, even if it finishes before setTimeout(function(){ run(); - }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); + }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); }); }else{ running = false; @@ -50,7 +50,7 @@ var JobQueue = function(){ end_of_queue.resolve(); } }; - + // adds a job to the schedule. // opts : { // duration : the job is guaranteed to finish no quicker than this (milisec) @@ -66,15 +66,15 @@ var JobQueue = function(){ // remove all jobs from the schedule (except the ones marked as important) this.clear = function(){ - queue = _.filter(queue,function(job){return job.opts.important === true;}); + queue = _.filter(queue,function(job){return job.opts.important === true;}); }; // end the repetition of the current job this.stoprepeat = function(){ stoprepeat = true; }; - - // returns a deferred that resolves when all scheduled + + // returns a deferred that resolves when all scheduled // jobs have been run. // ( jobs added after the call to this method are considered as well ) this.finished = function(){ @@ -86,7 +86,7 @@ var JobQueue = function(){ // 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. +// methods are used both to signal an event, and to fetch information. var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ init: function(parent,options){ @@ -95,7 +95,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ options = options || {}; this.pos = parent; - + this.weighting = false; this.debug_weight = 0; this.use_debug_weight = false; @@ -107,7 +107,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ payment_method: undefined, receipt_client: undefined, receipt_shop: undefined, - }; + }; this.custom_payment_status = this.default_payment_status; this.receipt_queue = []; @@ -115,7 +115,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ this.notifications = {}; this.bypass_proxy = false; - this.connection = null; + this.connection = null; this.host = ''; this.keptalive = false; @@ -170,9 +170,9 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ }, // find a proxy and connects to it. for options see find_proxy - // - force_ip : only try to connect to the specified ip. + // - force_ip : only try to connect to the specified ip. // - port: what port to listen to (default 8069) - // - progress(fac) : callback for search progress ( fac in [0,1] ) + // - progress(fac) : callback for search progress ( fac in [0,1] ) autoconnect: function(options){ var self = this; this.set_connection_status('connecting',{}); @@ -209,7 +209,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ var self = this; function status(){ - self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500}) + self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500}) .then(function(driver_status){ self.set_connection_status('connected',driver_status); },function(){ @@ -233,7 +233,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ callbacks[i](params); } if(this.get('status').status !== 'disconnected'){ - return this.connection.rpc('/hw_proxy/' + name, params || {}); + return this.connection.rpc('/hw_proxy/' + name, params || {}); }else{ return (new $.Deferred()).reject(); } @@ -283,7 +283,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ // returns as a deferred a valid host url that can be used as proxy. // options: // - port: what port to listen to (default 8069) - // - progress(fac) : callback for search progress ( fac in [0,1] ) + // - progress(fac) : callback for search progress ( fac in [0,1] ) find_proxy: function(options){ options = options || {}; var self = this; @@ -303,7 +303,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ urls.push('http://10.0.0.'+i+port); } - var prog_inc = 1/urls.length; + var prog_inc = 1/urls.length; function update_progress(){ progress = found ? 1 : progress + prog_inc; @@ -317,7 +317,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ done = done || new $.Deferred(); - if( !url || found || !self.searching_for_proxy ){ + if( !url || found || !self.searching_for_proxy ){ done.resolve(); return done; } @@ -325,7 +325,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ $.ajax({ url: url + '/hw_proxy/hello', method: 'GET', - timeout: 400, + timeout: 400, }).done(function(){ found = true; update_progress(); @@ -345,7 +345,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ for(i = 0; i < len; i++){ threads.push(thread()); } - + $.when.apply($,threads).then(function(){ var urls = []; for(var i = 0; i < arguments.length; i++){ @@ -364,7 +364,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ this.set_connection_status('disconnected'); }, - // this allows the client to be notified when a proxy call is made. The notification + // 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]){ @@ -372,8 +372,8 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ } this.notifications[name].push(callback); }, - - // returns the weight on the scale. + + // returns the weight on the scale. scale_read: function(){ var self = this; var ret = new $.Deferred(); @@ -389,7 +389,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ return ret; }, - // sets a custom weight, ignoring the proxy returned value. + // sets a custom weight, ignoring the proxy returned value. debug_set_weight: function(kg){ this.use_debug_weight = true; this.debug_weight = kg; @@ -406,7 +406,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ return this.message('open_cashbox'); }, - /* + /* * ask the printer to print a receipt */ print_receipt: function(receipt){ @@ -443,7 +443,7 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ }); // 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 +// is set-up to act like a keyboard. Use connect() and disconnect() to activate // and deactivate the barcode reader. Use set_action_callbacks to tell it // what to do when it reads a barcode. var BarcodeReader = core.Class.extend({ @@ -465,7 +465,6 @@ var BarcodeReader = core.Class.extend({ this.action_callback_stack = []; core.bus.on('barcode_scanned', this, function (barcode) { - console.log("got core.bus:" + barcode); this.scan(barcode); }); }, @@ -488,17 +487,17 @@ var BarcodeReader = core.Class.extend({ this.action_callback = callbacks; } }, - + // when a barcode is scanned and parsed, the callback corresponding - // to its type is called with the parsed_barcode as a parameter. - // (parsed_barcode is the result of parse_barcode(barcode)) - // + // to its type is called with the parsed_barcode as a parameter. + // (parsed_barcode is the result of parse_barcode(barcode)) + // // callbacks is a Map of 'actions' : callback(parsed_barcode) // that sets the callback for each action. if a callback for the - // specified action already exists, it is replaced. - // - // possible actions include : - // 'product' | 'cashier' | 'client' | 'discount' + // 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; @@ -510,7 +509,7 @@ var BarcodeReader = core.Class.extend({ } }, - //remove all action callbacks + //remove all action callbacks reset_action_callbacks: function(){ for(var action in this.action_callback){ this.action_callback[action] = undefined; @@ -555,9 +554,9 @@ var BarcodeReader = core.Class.extend({ function waitforbarcode(){ return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500}) .then(function(barcode){ - if(!self.remote_scanning){ + if(!self.remote_scanning){ self.remote_active = 0; - return; + return; } self.scan(barcode); waitforbarcode(); diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index 24e48732ede..91da131e88a 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -158,7 +158,7 @@ var Gui = core.Class.extend({ this.close_popup(); } this.current_popup = this.popup_instances[name]; - this.current_popup.show(options); + return this.current_popup.show(options); }, // close the current popup. diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 299274f5679..6823197df5c 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -360,7 +360,14 @@ exports.PosModel = Backbone.Model.extend({ } self.cashregisters = self.cashregisters.sort(function(a,b){ - return a.journal.sequence - b.journal.sequence; + // prefer cashregisters to be first in the list + if (a.journal.type == "cash" && b.journal.type != "cash") { + return -1; + } else if (a.journal.type != "cash" && b.journal.type == "cash") { + return 1; + } else { + return a.journal.sequence - b.journal.sequence; + } }); }, @@ -1357,7 +1364,7 @@ var OrderlineCollection = Backbone.Collection.extend({ }); // Every Paymentline contains a cashregister and an amount of money. -var Paymentline = Backbone.Model.extend({ +exports.Paymentline = Backbone.Model.extend({ initialize: function(attributes, options) { this.pos = options.pos; this.order = options.order; @@ -1419,7 +1426,7 @@ var Paymentline = Backbone.Model.extend({ }); var PaymentlineCollection = Backbone.Collection.extend({ - model: Paymentline, + model: exports.Paymentline, }); // An order more or less represents the content of a client's shopping cart (the OrderLines) @@ -1501,7 +1508,7 @@ exports.Order = Backbone.Model.extend({ var paymentlines = json.statement_ids; for (var i = 0; i < paymentlines.length; i++) { var paymentline = paymentlines[i][2]; - var newpaymentline = new Paymentline({},{pos: this.pos, order: this, json: paymentline}); + var newpaymentline = new exports.Paymentline({},{pos: this.pos, order: this, json: paymentline}); this.paymentlines.add(newpaymentline); if (i === paymentlines.length - 1) { @@ -1770,7 +1777,7 @@ exports.Order = Backbone.Model.extend({ /* ---- Payment Lines --- */ add_paymentline: function(cashregister) { this.assert_editable(); - var newPaymentline = new Paymentline({},{order: this, cashregister:cashregister, pos: this.pos}); + var newPaymentline = new exports.Paymentline({},{order: this, cashregister:cashregister, pos: this.pos}); if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){ newPaymentline.set_amount( Math.max(this.get_due(),0) ); } diff --git a/addons/point_of_sale/static/src/js/popups.js b/addons/point_of_sale/static/src/js/popups.js index 1a28061a931..4d0614bc682 100644 --- a/addons/point_of_sale/static/src/js/popups.js +++ b/addons/point_of_sale/static/src/js/popups.js @@ -243,4 +243,5 @@ var OrderImportPopupWidget = PopupWidget.extend({ }); gui.define_popup({name:'orderimport', widget: OrderImportPopupWidget}); +return PopupWidget; }); diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index fe9e5e7bc68..10e9ef66c1e 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -59,12 +59,12 @@ var ScreenWidget = PosBaseWidget.extend({ // it will add the product to the order and go to barcode_product_screen. barcode_product_action: function(code){ var self = this; - if(self.pos.scan_product(code)){ - if(self.barcode_product_screen){ + if (self.pos.scan_product(code)) { + if (self.barcode_product_screen) { self.gui.show_screen(self.barcode_product_screen); } - }else{ - self.gui.show_popup('error-barcode',code.code); + } else { + this.barcode_error_action(code); } }, @@ -81,7 +81,7 @@ var ScreenWidget = PosBaseWidget.extend({ return true; } } - this.gui.show_popup('error-barcode',code.code); + this.barcode_error_action(code); return false; }, @@ -95,7 +95,7 @@ var ScreenWidget = PosBaseWidget.extend({ this.pos.get_order().set_client(partner); return true; } - this.gui.show_popup('error-barcode',code.code); + this.barcode_error_action(code); return false; }, @@ -108,8 +108,14 @@ var ScreenWidget = PosBaseWidget.extend({ } }, // What happens when an invalid barcode is scanned : shows an error popup. - barcode_error_action: function(code){ - this.gui.show_popup('error-barcode',code.code); + barcode_error_action: function(code) { + var show_code; + if (code.code.length > 32) { + show_code = code.code.substring(0,29)+'...'; + } else { + show_code = code.code; + } + this.gui.show_popup('error-barcode',show_code); }, // this method shows the screen and sets up all the widget related to this screen. Extend this method @@ -123,11 +129,11 @@ var ScreenWidget = PosBaseWidget.extend({ } this.pos.barcode_reader.set_action_callback({ - 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined , - 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined , - 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined , - 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined, - 'error' : self.barcode_error_action ? function(code){ self.barcode_error_action(code); } : undefined, + 'cashier': _.bind(self.barcode_cashier_action, self), + 'product': _.bind(self.barcode_product_action, self), + 'client' : _.bind(self.barcode_client_action, self), + 'discount': _.bind(self.barcode_discount_action, self), + 'error' : _.bind(self.barcode_error_action, self), }); }, @@ -1562,10 +1568,20 @@ var PaymentScreenWidget = ScreenWidget.extend({ } }, click_numpad: function(button) { - if (!this.pos.get_order().get_paymentlines().length) { - this.pos.get_order().add_paymentline( this.pos.cashregisters[0]); - this.render_paymentlines(); - } + var paymentlines = this.pos.get_order().get_paymentlines(); + var open_paymentline = false; + + for (var i = 0; i < paymentlines.length; i++) { + if (! paymentlines[i].paid) { + open_paymentline = true; + } + } + + if (! open_paymentline) { + this.pos.get_order().add_paymentline( this.pos.cashregisters[0]); + this.render_paymentlines(); + } + this.payment_input(button.data('action')); }, render_numpad: function() { @@ -1890,6 +1906,7 @@ return { ActionButtonWidget: ActionButtonWidget, define_action_button: define_action_button, ScreenWidget: ScreenWidget, + PaymentScreenWidget: PaymentScreenWidget, OrderWidget: OrderWidget, }; diff --git a/addons/pos_mercury/__init__.py b/addons/pos_mercury/__init__.py new file mode 100644 index 00000000000..81fc81d29c9 --- /dev/null +++ b/addons/pos_mercury/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import models diff --git a/addons/pos_mercury/__openerp__.py b/addons/pos_mercury/__openerp__.py new file mode 100644 index 00000000000..4b60cb2f0a3 --- /dev/null +++ b/addons/pos_mercury/__openerp__.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Mercury Payment Services', + 'version': '1.0', + 'category': 'Point of Sale', + 'sequence': 6, + 'summary': 'Credit card support for Point Of Sale', + 'description': """ +Allow credit card POS payments +============================== + +This module allows customers to pay for their orders with credit +cards. The transactions are processed by Mercury (developed by Wells +Fargo Bank). A Mercury merchant account is necessary. It allows the +following: + +* Fast payment by just swiping a credit card while on the payment screen +* Combining of cash payments and credit card payments +* Cashback +* Supported cards: Visa, MasterCard, American Express, Discover + """, + 'author': 'Odoo SA', + 'depends': ['web', 'barcodes', 'point_of_sale'], + 'website': '', + 'data': [ + 'data/pos_mercury_data.xml', + 'security/ir.model.access.csv', + 'views/pos_mercury_templates.xml', + 'views/pos_mercury_views.xml', + 'views/pos_mercury_transaction_templates.xml', + ], + 'demo': [ + 'data/pos_mercury_demo.xml', + ], + 'qweb': [ + 'static/src/xml/pos_mercury.xml', + ], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/pos_mercury/data/pos_mercury_data.xml b/addons/pos_mercury/data/pos_mercury_data.xml new file mode 100644 index 00000000000..2321ac65655 --- /dev/null +++ b/addons/pos_mercury/data/pos_mercury_data.xml @@ -0,0 +1,13 @@ + + + + + Magnetic Credit Card + + 85 + credit + any + %.* + + + diff --git a/addons/pos_mercury/data/pos_mercury_demo.xml b/addons/pos_mercury/data/pos_mercury_demo.xml new file mode 100644 index 00000000000..fad7adac136 --- /dev/null +++ b/addons/pos_mercury/data/pos_mercury_demo.xml @@ -0,0 +1,12 @@ + + + + + + + Mercury Demo + 334160 + 81303DUR + + + diff --git a/addons/pos_mercury/models/__init__.py b/addons/pos_mercury/models/__init__.py new file mode 100644 index 00000000000..521e053629a --- /dev/null +++ b/addons/pos_mercury/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import pos_mercury +import pos_mercury_transaction diff --git a/addons/pos_mercury/models/pos_mercury.py b/addons/pos_mercury/models/pos_mercury.py new file mode 100644 index 00000000000..63f531ea230 --- /dev/null +++ b/addons/pos_mercury/models/pos_mercury.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import sets + +from openerp import models, fields, api + +_logger = logging.getLogger(__name__) + +class barcode_rule(models.Model): + _inherit = 'barcode.rule' + + def _get_type_selection(self): + types = sets.Set(super(barcode_rule, self)._get_type_selection()) + types.update([ + ('credit', 'Credit Card') + ]) + return list(types) + + +class pos_mercury_payment_data(models.Model): + _name = 'pos_mercury.configuration' + + # FIELDS # + name = fields.Char(required=True, help='Name of this Mercury configuration') + merchant_id = fields.Char(string='Merchant ID', required=True, help='ID of the merchant to authenticate him on the payment provider server') + merchant_pwd = fields.Char(string='Merchant Password', required=True, help='Password of the merchant to authenticate him on the payment provider server') + +class account_bank_statement_line(models.Model): + _inherit = "account.bank.statement.line" + + mercury_card_number = fields.Char(string='Card Number', help='The last 4 numbers of the card used to pay') + mercury_prefixed_card_number = fields.Char(string='Card Number', compute='_compute_prefixed_card_number', help='The card number used for the payment.') + mercury_card_brand = fields.Char(string='Card Brand', help='The brand of the payment card (e.g. Visa, AMEX, ...)') + mercury_card_owner_name = fields.Char(string='Card Owner Name', help='The name of the card owner') + mercury_ref_no = fields.Char(string='Mercury reference number', help='Payment reference number from Mercury Pay') + mercury_record_no = fields.Char(string='Mercury record number', help='Payment record number from Mercury Pay') + mercury_invoice_no = fields.Integer(string='Mercury invoice number', help='Invoice number from Mercury Pay') + + @api.one + def _compute_prefixed_card_number(self): + if self.mercury_card_number: + self.mercury_prefixed_card_number = "********" + self.mercury_card_number + else: + self.mercury_prefixed_card_number = "" + +class account_journal(models.Model): + _inherit = 'account.journal' + + pos_mercury_config_id = fields.Many2one('pos_mercury.configuration', string='Mercury configuration', help='The configuration of Mercury used for this journal') + +class pos_order_card(models.Model): + _inherit = "pos.order" + + @api.model + def _payment_fields(self, ui_paymentline): + fields = super(pos_order_card, self)._payment_fields(ui_paymentline) + + fields.update({ + 'card_number': ui_paymentline.get('mercury_card_number'), + 'card_brand': ui_paymentline.get('mercury_card_brand'), + 'card_owner_name': ui_paymentline.get('mercury_card_owner_name'), + 'ref_no': ui_paymentline.get('mercury_ref_no'), + 'record_no': ui_paymentline.get('mercury_record_no'), + 'invoice_no': ui_paymentline.get('mercury_invoice_no') + }) + + return fields + + @api.model + def add_payment(self, order_id, data): + statement_id = super(pos_order_card, self).add_payment(order_id, data) + statement_lines = self.env['account.bank.statement.line'].search([('statement_id', '=', statement_id), + ('pos_statement_id', '=', order_id), + ('journal_id', '=', data['journal']), + ('amount', '=', data['amount'])]) + + # we can get multiple statement_lines when there are >1 credit + # card payments with the same amount. In that case it doesn't + # matter which statement line we pick, just pick one that + # isn't already used. + for line in statement_lines: + if not line.mercury_card_brand: + line.mercury_card_brand = data.get('card_brand') + line.mercury_card_number = data.get('card_number') + line.mercury_card_owner_name = data.get('card_owner_name') + + line.mercury_ref_no = data.get('ref_no') + line.mercury_record_no = data.get('record_no') + line.mercury_invoice_no = data.get('invoice_no') + + break + + return statement_id diff --git a/addons/pos_mercury/models/pos_mercury_transaction.py b/addons/pos_mercury/models/pos_mercury_transaction.py new file mode 100644 index 00000000000..05d7efdf265 --- /dev/null +++ b/addons/pos_mercury/models/pos_mercury_transaction.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import cgi +import urllib2 +import ssl +import werkzeug +from datetime import date, timedelta + +from openerp import models, api, service +from openerp.exceptions import UserError +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT + +class MercuryTransaction(models.Model): + _name = 'pos_mercury.mercury_transaction' + + def _get_pos_session(self): + pos_session = self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)]) + if not pos_session: + raise UserError("No POS session") + + pos_session.login() + + return pos_session + + def _get_pos_mercury_config_id(self, config, journal_id): + journal = config.journal_ids.filtered(lambda r: r.id == journal_id) + + if journal and journal.pos_mercury_config_id: + return journal.pos_mercury_config_id + else: + raise UserError("No Mercury configuration associated with the journal.") + + def _setup_request(self, data): + pos_session = self._get_pos_session() + + config = pos_session.config_id + pos_mercury_config = self._get_pos_mercury_config_id(config, data['journal_id']) + + data['operator_id'] = pos_session.user_id.login + data['merchant_id'] = pos_mercury_config.sudo().merchant_id + data['merchant_pwd'] = pos_mercury_config.sudo().merchant_pwd + data['memo'] = "Odoo " + service.common.exp_version()['server_version'] + + def _do_request(self, template, data): + xml_transaction = self.env.ref(template).render(data) + + if not data['merchant_id'] or not data['merchant_pwd']: + return "not setup" + + soap_header = '' + soap_footer = '' + data['merchant_pwd'] + '' + xml_transaction = soap_header + cgi.escape(xml_transaction) + soap_footer + + response = '' + + headers = { + 'Content-Type': 'text/xml', + 'SOAPAction': 'http://www.mercurypay.com/CreditTransaction', + } + + r = urllib2.Request('https://w1.mercurypay.com/ws/ws.asmx', data=xml_transaction, headers=headers) + try: + u = urllib2.urlopen(r, timeout=65) + response = werkzeug.utils.unescape(u.read()) + except (urllib2.URLError, ssl.SSLError): + response = "timeout" + + return response + + def _do_reversal_or_voidsale(self, data, is_voidsale): + try: + self._setup_request(data) + except UserError: + return "internal error" + + data['is_voidsale'] = is_voidsale + response = self._do_request('pos_mercury.mercury_voidsale', data) + return response + + @api.model + def do_payment(self, data): + try: + self._setup_request(data) + except UserError: + return "internal error" + + response = self._do_request('pos_mercury.mercury_transaction', data) + return response + + @api.model + def do_reversal(self, data): + return self._do_reversal_or_voidsale(data, False) + + @api.model + def do_voidsale(self, data): + return self._do_reversal_or_voidsale(data, True) + + def do_return(self, data): + try: + self._setup_request(data) + except UserError: + return "internal error" + + response = self._do_request('pos_mercury.mercury_return', data) + return response + + # One time (the ones we use) Mercury tokens are required to be + # deleted after 6 months + @api.model + def cleanup_old_tokens(self): + expired_creation_date = (date.today() - timedelta(days=6 * 30)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) + + for order in self.env['pos.order'].search([('create_date', '<', expired_creation_date)]): + order.ref_no = "" + order.record_no = "" diff --git a/addons/pos_mercury/security/ir.model.access.csv b/addons/pos_mercury/security/ir.model.access.csv new file mode 100644 index 00000000000..f2708f8262b --- /dev/null +++ b/addons/pos_mercury/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_pos_mercury_configuration,mercury.configuration,model_pos_mercury_configuration,point_of_sale.group_pos_manager,1,1,1,1 +access_pos_mercury_mercury_transaction,mercury.transaction,model_pos_mercury_mercury_transaction,,1,0,0,0 \ No newline at end of file diff --git a/addons/pos_mercury/static/src/css/pos_mercury.css b/addons/pos_mercury/static/src/css/pos_mercury.css new file mode 100644 index 00000000000..6d48663fc19 --- /dev/null +++ b/addons/pos_mercury/static/src/css/pos_mercury.css @@ -0,0 +1,7 @@ +.pos .paymentline.selected.o_pos_mercury_swipe_pending, .pos .paymentline.o_pos_mercury_swipe_pending { + background: rgb(239, 153, 65); +} +.pos .col-tendered.edit.o_pos_mercury_swipe_pending { + color: rgb(239, 153, 65); + box-shadow: 0px 0px 0px 3px rgb(239, 153, 65); +} \ No newline at end of file diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js new file mode 100644 index 00000000000..89af16b52d9 --- /dev/null +++ b/addons/pos_mercury/static/src/js/pos_mercury.js @@ -0,0 +1,675 @@ +odoo.define('pos_mercury.pos_mercury', function (require) { +"use strict"; + +var Class = require('web.Class'); +var Model = require('web.Model'); +var session = require('web.session'); +var core = require('web.core'); +var screens = require('point_of_sale.screens'); +var gui = require('point_of_sale.gui'); +var pos_model = require('point_of_sale.models'); +var utils = require('web.utils'); + +var _t = core._t; + +var BarcodeParser = require('barcodes.BarcodeParser'); +var PopupWidget = require('point_of_sale.popups'); +var ScreenWidget = screens.ScreenWidget; +var PaymentScreenWidget = screens.PaymentScreenWidget; +var round_pr = utils.round_precision; + +var onlinePaymentJournal = []; + +var _modelproto = pos_model.PosModel.prototype; +pos_model.PosModel = pos_model.PosModel.extend({ + allowOnlinePayment: function () { + if (onlinePaymentJournal.length) { + return true; + } + $.each(this.journals, function (i, val) { + if (val.pos_mercury_config_id) { + onlinePaymentJournal.push({label:val.display_name, item:val.id}); + } + }); + return onlinePaymentJournal.length; + }, + getCashRegisterByJournalID: function (journal_id) { + var cashregister_return; + + $.each(this.cashregisters, function (index, cashregister) { + if (cashregister.journal_id[0] === journal_id) { + cashregister_return = cashregister; + } + }); + + return cashregister_return; + }, + decodeMagtek: function (magtekInput) { + // Regular expression to identify and extract data from the track 1 & 2 of the magnetic code + var _track1_regex = /%B?([0-9]*)\^([A-Z\/ -_]*)\^([0-9]{4})(.{3})([^?]+)\?/; + + var track1 = magtekInput.match(_track1_regex); + var magtek_generated = magtekInput.split('|'); + + var to_return = {}; + try { + track1.shift(); // get rid of complete match + to_return['number'] = track1.shift().substr(-4); + to_return['name'] = track1.shift(); + track1.shift(); // expiration date + track1.shift(); // service code + track1.shift(); // discretionary data + track1.shift(); // zero pad + + magtek_generated.shift(); // track1 and track2 + magtek_generated.shift(); // clear text crc + magtek_generated.shift(); // encryption counter + to_return['encrypted_block'] = magtek_generated.shift(); + magtek_generated.shift(); // enc session id + magtek_generated.shift(); // device serial + magtek_generated.shift(); // magneprint data + magtek_generated.shift(); // magneprint status + magtek_generated.shift(); // enc track3 + to_return['encrypted_key'] = magtek_generated.shift(); + magtek_generated.shift(); // enc track1 + magtek_generated.shift(); // reader enc status + + return to_return; + } catch (e) { + return 0; + } + }, + decodeMercuryResponse: function (data) { + // get rid of xml version declaration and just keep the RStream + // from the response because the xml contains two version + // declarations. One for the SOAP, and one for the content. Maybe + // we should unpack the SOAP layer in python? + data = data.replace(/.*<\?xml version="1.0"\?>/, ""); + data = data.replace(/<\/CreditTransactionResult>.*/, ""); + + var xml = $($.parseXML(data)); + var cmd_response = xml.find("CmdResponse"); + var tran_response = xml.find("TranResponse"); + + return { + status: cmd_response.find("CmdStatus").text(), + message: cmd_response.find("TextResponse").text(), + error: cmd_response.find("DSIXReturnCode").text(), + card_type: tran_response.find("CardType").text(), + auth_code: tran_response.find("AuthCode").text(), + acq_ref_data: tran_response.find("AcqRefData").text(), + process_data: tran_response.find("ProcessData").text(), + invoice_no: tran_response.find("InvoiceNo").text(), + ref_no: tran_response.find("RefNo").text(), + record_no: tran_response.find("RecordNo").text(), + purchase: parseFloat(tran_response.find("Purchase").text()), + authorize: parseFloat(tran_response.find("Authorize").text()), + }; + } +}); + +var _paylineproto = pos_model.Paymentline.prototype; +pos_model.Paymentline = pos_model.Paymentline.extend({ + init_from_JSON: function (json) { + _paylineproto.init_from_JSON.apply(this, arguments); + + this.paid = json.paid; + this.mercury_card_number = json.mercury_card_number; + this.mercury_card_brand = json.mercury_card_brand; + this.mercury_card_owner_name = json.mercury_card_owner_name; + this.mercury_ref_no = json.mercury_ref_no; + this.mercury_record_no = json.mercury_record_no; + this.mercury_invoice_no = json.mercury_invoice_no; + this.mercury_auth_code = json.mercury_auth_code; + this.mercury_data = json.mercury_data; + this.mercury_swipe_pending = json.mercury_swipe_pending; + + this.set_credit_card_name(); + }, + export_as_JSON: function () { + return _.extend(_paylineproto.export_as_JSON.apply(this, arguments), {paid: this.paid, + mercury_card_number: this.mercury_card_number, + mercury_card_brand: this.mercury_card_brand, + mercury_card_owner_name: this.mercury_card_owner_name, + mercury_ref_no: this.mercury_ref_no, + mercury_record_no: this.mercury_record_no, + mercury_invoice_no: this.mercury_invoice_no, + mercury_auth_code: this.mercury_auth_code, + mercury_data: this.mercury_data, + mercury_swipe_pending: this.mercury_swipe_pending}); + }, + set_credit_card_name: function () { + if (this.mercury_card_number) { + this.name = this.mercury_card_brand + " (****" + this.mercury_card_number + ")"; + } + } +}); + +// Lookup table to store status and error messages +var lookUpCodeTransaction = { + 'Approved': { + '000000': _t('Transaction approved'), + }, + 'TimeoutError': { + '001006': _t('Global API Not Initialized'), + '001007': _t('Timeout on Response'), + '003003': _t('Socket Error sending request'), + '003004': _t('Socket already open or in use'), + '003005': _t('Socket Creation Failed'), + '003006': _t('Socket Connection Failed'), + '003007': _t('Connection Lost'), + '003008': _t('TCP/IP Failed to Initialize'), + '003010': _t('Time Out waiting for server response'), + '003011': _t('Connect Canceled'), + '003053': _t('Initialize Failed'), + '009999': _t('Unknown Error'), + }, + 'FatalError': { + '-1': _t('Timeout error'), + '001001': _t('General Failure'), + '001003': _t('Invalid Command Format'), + '001004': _t('Insufficient Fields'), + '001011': _t('Empty Command String'), + '002000': _t('Password Verified'), + '002001': _t('Queue Full'), + '002002': _t('Password Failed – Disconnecting'), + '002003': _t('System Going Offline'), + '002004': _t('Disconnecting Socket'), + '002006': _t('Refused ‘Max Connections’'), + '002008': _t('Duplicate Serial Number Detected'), + '002009': _t('Password Failed (Client / Server)'), + '002010': _t('Password failed (Challenge / Response)'), + '002011': _t('Internal Server Error – Call Provider'), + '003002': _t('In Process with server'), + '003009': _t('Control failed to find branded serial (password lookup failed)'), + '003012': _t('128 bit CryptoAPI failed'), + '003014': _t('Threaded Auth Started Expect Response'), + '003017': _t('Failed to start Event Thread.'), + '003050': _t('XML Parse Error'), + '003051': _t('All Connections Failed'), + '003052': _t('Server Login Failed'), + '004001': _t('Global Response Length Error (Too Short)'), + '004002': _t('Unable to Parse Response from Global (Indistinguishable)'), + '004003': _t('Global String Error'), + '004004': _t('Weak Encryption Request Not Supported'), + '004005': _t('Clear Text Request Not Supported'), + '004010': _t('Unrecognized Request Format'), + '004011': _t('Error Occurred While Decrypting Request'), + '004017': _t('Invalid Check Digit'), + '004018': _t('Merchant ID Missing'), + '004019': _t('TStream Type Missing'), + '004020': _t('Could Not Encrypt Response- Call Provider'), + '100201': _t('Invalid Transaction Type'), + '100202': _t('Invalid Operator ID'), + '100203': _t('Invalid Memo'), + '100204': _t('Invalid Account Number'), + '100205': _t('Invalid Expiration Date'), + '100206': _t('Invalid Authorization Code'), + '100207': _t('Invalid Authorization Code'), + '100208': _t('Invalid Authorization Amount'), + '100209': _t('Invalid Cash Back Amount'), + '100210': _t('Invalid Gratuity Amount'), + '100211': _t('Invalid Purchase Amount'), + '100212': _t('Invalid Magnetic Stripe Data'), + '100213': _t('Invalid PIN Block Data'), + '100214': _t('Invalid Derived Key Data'), + '100215': _t('Invalid State Code'), + '100216': _t('Invalid Date of Birth'), + '100217': _t('Invalid Check Type'), + '100218': _t('Invalid Routing Number'), + '100219': _t('Invalid TranCode'), + '100220': _t('Invalid Merchant ID'), + '100221': _t('Invalid TStream Type'), + '100222': _t('Invalid Batch Number'), + '100223': _t('Invalid Batch Item Count'), + '100224': _t('Invalid MICR Input Type'), + '100225': _t('Invalid Driver’s License'), + '100226': _t('Invalid Sequence Number'), + '100227': _t('Invalid Pass Data'), + '100228': _t('Invalid Card Type'), + }, +}; +// Popup to show all transaction state for the payment. + +var PaymentTransactionPopupWidget = PopupWidget.extend({ + template: 'PaymentTransactionPopupWidget', + show: function (options) { + var self = this; + this._super(options); + options.transaction.then(function (data) { + if (data.auto_close) { + setTimeout(function () { + self.gui.close_popup(); + }, 2000); + } else { + self.close(); + self.$el.find('.popup').append(''); + } + + self.$el.find('p.body').html(data.message); + }).progress(function (data) { + self.$el.find('p.body').html(data.message); + }); + } +}); + +gui.define_popup({name:'payment-transaction', widget: PaymentTransactionPopupWidget}); + +// On all screens, if a card is swipped, return a popup error. +ScreenWidget.include({ + credit_error_action: function () { + this.gui.show_popup('error-barcode',_t('Go to payment screen to use cards')); + }, + + show: function () { + this._super(); + if(this.pos.allowOnlinePayment()) { + this.pos.barcode_reader.set_action_callback('credit', _.bind(this.credit_error_action, this)); + } + } +}); + +// On Payment screen, allow electronic payments +PaymentScreenWidget.include({ + // How long we wait for the odoo server to deliver the response of + // a Mercury transaction + server_timeout_in_ms: 95000, + + // How many Mercury transactions we send without receiving a + // response + server_retries: 3, + + _get_swipe_pending_line: function () { + var i = 0; + var lines = this.pos.get_order().get_paymentlines(); + + for (i = 0; i < lines.length; i++) { + if (lines[i].mercury_swipe_pending) { + return lines[i]; + } + } + + return 0; + }, + + _does_credit_payment_line_exist: function (amount, card_number, card_brand, card_owner_name) { + var i = 0; + var lines = this.pos.get_order().get_paymentlines(); + + for (i = 0; i < lines.length; i++) { + if (lines[i].mercury_amount === amount && + lines[i].mercury_card_number === card_number && + lines[i].mercury_card_brand === card_brand && + lines[i].mercury_card_owner_name === card_owner_name) { + return true; + } + } + + return false; + }, + + retry_mercury_transaction: function (def, response, retry_nr, can_connect_to_server, callback, args) { + var self = this; + var message = ""; + + if (retry_nr < self.server_retries) { + if (response) { + message = "Retry #" + (retry_nr + 1) + "...

" + response.message; + } else { + message = "Retry #" + (retry_nr + 1) + "..."; + } + def.notify({ + message: message + }); + + setTimeout(function () { + callback.apply(self, args); + }, 1000); + } else { + if (response) { + message = "Error " + response.error + ": " + lookUpCodeTransaction["TimeoutError"][response.error] + "
" + response.message; + } else { + if (can_connect_to_server) { + message = _t("No response from Mercury (Mercury down?)"); + } else { + message = _t("No response from server (connected to network?)"); + } + } + def.resolve({ + message: message, + auto_close: false + }); + } + }, + + // Handler to manage the card reader string + credit_code_transaction: function (parsed_result, old_deferred, retry_nr) { + if(!this.pos.allowOnlinePayment()) { + return; + } + + var self = this; + var decodedMagtek = self.pos.decodeMagtek(parsed_result.code); + + if (! decodedMagtek) { + this.gui.show_popup('error',{ + 'title': 'Could not read card', + 'body': _t('This can be caused by a badly executed swipe or by not having your keyboard layout set to US QWERTY (not US International).'), + }); + return; + } + + var swipe_pending_line = self._get_swipe_pending_line(); + var purchase_amount = 0; + + if (swipe_pending_line) { + purchase_amount = swipe_pending_line.get_amount(); + } else { + purchase_amount = self.pos.get_order().get_due(); + } + + var transaction = { + 'encrypted_key' : decodedMagtek['encrypted_key'], + 'encrypted_block' : decodedMagtek['encrypted_block'], + 'transaction_type' : 'Credit', + 'transaction_code' : 'Sale', + 'invoice_no' : self.pos.get_order().uid.replace(/-/g,''), + 'purchase' : purchase_amount, + 'journal_id' : parsed_result.journal_id, + }; + + var def = old_deferred || new $.Deferred(); + retry_nr = retry_nr || 0; + + // show the transaction popup. + // the transaction deferred is used to update transaction status + // if we have a previous deferred it indicates that this is a retry + if (! old_deferred) { + self.gui.show_popup('payment-transaction', { + transaction: def + }); + def.notify({ + message: _t('Handling transaction...'), + }); + } + + var mercury_transaction = new Model('pos_mercury.mercury_transaction'); + mercury_transaction.call('do_payment', [transaction], undefined, {timeout: self.server_timeout_in_ms}).then(function (data) { + // if not receiving a response from Mercury, we should retry + if (data === "timeout") { + self.retry_mercury_transaction(def, null, retry_nr, true, self.credit_code_transaction, [parsed_result, def, retry_nr + 1]); + return; + } + + if (data === "not setup") { + def.resolve({ + message: _t("Please setup your Mercury merchant account.") + }); + return; + } + + if (data === "internal error") { + def.resolve({ + message: _t("Odoo error while processing transaction.") + }); + return; + } + + var response = self.pos.decodeMercuryResponse(data); + response.journal_id = parsed_result.journal_id; + + if (response.status === 'Approved') { + // AP* indicates a duplicate request, so don't add anything for those + if (response.message === "AP*" && self._does_credit_payment_line_exist(response.authorize, decodedMagtek['number'], + response.card_type, decodedMagtek['name'])) { + def.resolve({ + message: lookUpCodeTransaction["Approved"][response.error], + auto_close: true, + }); + } else { + // If the payment is approved, add a payment line + var order = self.pos.get_order(); + + if (swipe_pending_line) { + order.select_paymentline(swipe_pending_line); + } else { + order.add_paymentline(self.pos.getCashRegisterByJournalID(parsed_result.journal_id)); + } + + order.selected_paymentline.paid = true; + order.selected_paymentline.mercury_swipe_pending = false; + order.selected_paymentline.mercury_amount = response.authorize; + order.selected_paymentline.mercury_card_number = decodedMagtek['number']; + order.selected_paymentline.mercury_card_brand = response.card_type; + order.selected_paymentline.mercury_card_owner_name = decodedMagtek['name']; + order.selected_paymentline.mercury_ref_no = response.ref_no; + order.selected_paymentline.mercury_record_no = response.record_no; + order.selected_paymentline.mercury_invoice_no = response.invoice_no; + order.selected_paymentline.mercury_auth_code = response.auth_code; + order.selected_paymentline.mercury_data = response; // used to reverse transactions + order.selected_paymentline.set_credit_card_name(); + + self.order_changes(); + self.reset_input(); + self.render_paymentlines(); + order.trigger('change', order); // needed so that export_to_JSON gets triggered + + if (response.message === "PARTIAL AP") { + def.resolve({ + message: _t("Partially approved"), + auto_close: false, + }); + } else { + def.resolve({ + message: lookUpCodeTransaction["Approved"][response.error], + auto_close: true, + }); + } + } + } + + // if an error related to timeout or connectivity issues arised, then retry the same transaction + else { + if (lookUpCodeTransaction["TimeoutError"][response.error]) { // recoverable error + self.retry_mercury_transaction(def, response, retry_nr, true, self.credit_code_transaction, [parsed_result, def, retry_nr + 1]); + } else { // not recoverable + def.resolve({ + message: "Error " + response.error + ":
" + response.message, + auto_close: false + }); + } + } + + }).fail(function (error, event) { + event.preventDefault(); + self.retry_mercury_transaction(def, null, retry_nr, false, self.credit_code_transaction, [parsed_result, def, retry_nr + 1]); + }); + }, + + credit_code_cancel: function () { + return; + }, + + credit_code_action: function (parsed_result) { + self = this; + + if (onlinePaymentJournal.length === 1) { + parsed_result.journal_id = onlinePaymentJournal[0].item; + self.credit_code_transaction(parsed_result); + } else { // this is for supporting another payment system like mercury + this.gui.show_popup('selection',{ + title: 'Pay ' + this.pos.get_order().get_due().toFixed(2) + ' with : ', + list: onlinePaymentJournal, + confirm: function (item) { + parsed_result.journal_id = item; + self.credit_code_transaction(parsed_result); + }, + cancel: self.credit_code_cancel, + }); + } + }, + + remove_paymentline_by_ref: function (line) { + this.pos.get_order().remove_paymentline(line); + this.reset_input(); + this.render_paymentlines(); + }, + + do_reversal: function (line, is_voidsale, old_deferred, retry_nr) { + var def = old_deferred || new $.Deferred(); + var self = this; + retry_nr = retry_nr || 0; + + // show the transaction popup. + // the transaction deferred is used to update transaction status + this.gui.show_popup('payment-transaction', { + transaction: def + }); + + var request_data = _.extend({ + 'transaction_type': 'Credit', + 'transaction_code': 'VoidSaleByRecordNo', + }, line.mercury_data); + + var message = ""; + var rpc_method = ""; + + if (is_voidsale) { + message = _t("Reversal failed, sending VoidSale..."); + rpc_method = "do_voidsale"; + } else { + message = _t("Sending reversal..."); + rpc_method = "do_reversal"; + } + + if (! old_deferred) { + def.notify({ + message: message, + }); + } + + var mercury_transaction = new Model('pos_mercury.mercury_transaction'); + mercury_transaction.call(rpc_method, [request_data], undefined, {timeout: self.server_timeout_in_ms}).then(function (data) { + if (data === "timeout") { + self.retry_mercury_transaction(def, null, retry_nr, true, self.do_reversal, [line, is_voidsale, def, retry_nr + 1]); + return; + } + + if (data === "internal error") { + def.resolve({ + message: _t("Odoo error while processing transaction.") + }); + return; + } + + var response = self.pos.decodeMercuryResponse(data); + + if (! is_voidsale) { + if (response.status != 'Approved' || response.message != 'REVERSED') { + // reversal was not successful, send voidsale + self.do_reversal(line, true); + } else { + // reversal was successful + def.resolve({ + message: _t("Reversal succeeded"), + }); + + self.remove_paymentline_by_ref(line); + } + } else { // voidsale ended, nothing more we can do + if (response.status === 'Approved') { + def.resolve({ + message: _t("VoidSale succeeded"), + }); + + self.remove_paymentline_by_ref(line); + } else { + def.resolve({ + message: "Error " + response.error + ":
" + response.message, + }); + } + } + }).fail(function (error, event) { + event.preventDefault(); + self.retry_mercury_transaction(def, null, retry_nr, false, self.do_reversal, [line, is_voidsale, def, retry_nr + 1]); + }); + }, + + click_delete_paymentline: function (cid) { + var self = this; + var lines = this.pos.get_order().get_paymentlines(); + + for (var i = 0; i < lines.length; i++) { + if (lines[i].cid === cid && lines[i].mercury_data) { + this.do_reversal(lines[i], false); + return; + } + } + + this._super(cid); + }, + + // make sure there is only one paymentline waiting for a swipe + click_paymentmethods: function (id) { + var i; + var order = this.pos.get_order(); + var cashregister = null; + for (i = 0; i < this.pos.cashregisters.length; i++) { + if (this.pos.cashregisters[i].journal_id[0] === id){ + cashregister = this.pos.cashregisters[i]; + break; + } + } + + if (cashregister.journal.type === 'bank') { + var already_swipe_pending = false; + var lines = order.get_paymentlines(); + + for (i = 0; i < lines.length; i++) { + if (lines[i].cashregister.journal.type === 'bank' && lines[i].mercury_swipe_pending) { + already_swipe_pending = true; + } + } + + if (already_swipe_pending) { + this.gui.show_popup('error',{ + 'title': 'Error', + 'body': _t('One credit card swipe already pending.'), + }); + } else { + this._super(id); + order.selected_paymentline.mercury_swipe_pending = true; + this.render_paymentlines(); + order.trigger('change', order); // needed so that export_to_JSON gets triggered + } + } else { + this._super(id); + } + }, + + show: function () { + this._super(); + if (this.pos.allowOnlinePayment()) { + this.pos.barcode_reader.set_action_callback('credit', _.bind(this.credit_code_action, this)); + } + }, + + // before validating, get rid of any paymentlines that are waiting + // on a swipe. + validate_order: function(force_validation) { + if (this.pos.get_order().is_paid() && ! this.invoicing) { + var lines = this.pos.get_order().get_paymentlines(); + + for (var i = 0; i < lines.length; i++) { + if (lines[i].mercury_swipe_pending) { + this.pos.get_order().remove_paymentline(lines[i]); + this.render_paymentlines(); + } + } + } + + this._super(); + } +}); + +}); diff --git a/addons/pos_mercury/static/src/xml/pos_mercury.xml b/addons/pos_mercury/static/src/xml/pos_mercury.xml new file mode 100644 index 00000000000..bb8071a5a59 --- /dev/null +++ b/addons/pos_mercury/static/src/xml/pos_mercury.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + +
WAITING FOR SWIPE
+
+ + + + + + + + + + this.removeAttr("class"); + this.attr('t-attf-class', 'paymentline selected #{line.mercury_swipe_pending ? "o_pos_mercury_swipe_pending" : ""}'); + + + this.removeAttr("class"); + this.attr('t-attf-class', 'paymentline #{line.mercury_swipe_pending ? "o_pos_mercury_swipe_pending" : ""}'); + + + this.removeAttr("class"); + this.attr('t-attf-class', 'col-tendered edit #{line.mercury_swipe_pending ? "o_pos_mercury_swipe_pending" : ""}'); + +
+ + + + +
+
CARDHOLDER WILL PAY CARD ISSUER
+
ABOVE AMOUNT PURSUANT
+
TO CARDHOLDER AGREEMENT
+
+
+
X______________________________
+ + +
+
+ + + + + + +
  APPROVAL CODE:
+
+
+
+ +
+ +
+
+
+ + + + + + + &nbsp;&nbsp;APPROVAL CODE: + + + + + +
+ +
+
+
+
diff --git a/addons/pos_mercury/views/pos_mercury_templates.xml b/addons/pos_mercury/views/pos_mercury_templates.xml new file mode 100644 index 00000000000..5206bfba606 --- /dev/null +++ b/addons/pos_mercury/views/pos_mercury_templates.xml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/addons/pos_mercury/views/pos_mercury_transaction_templates.xml b/addons/pos_mercury/views/pos_mercury_transaction_templates.xml new file mode 100644 index 00000000000..752208b64e8 --- /dev/null +++ b/addons/pos_mercury/views/pos_mercury_transaction_templates.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + diff --git a/addons/pos_mercury/views/pos_mercury_views.xml b/addons/pos_mercury/views/pos_mercury_views.xml new file mode 100644 index 00000000000..76271909f29 --- /dev/null +++ b/addons/pos_mercury/views/pos_mercury_views.xml @@ -0,0 +1,122 @@ + + + + + Mercury Configurations + pos_mercury.configuration + +
+ +
+
+
+

+ Mercury Configurations define what Mercury account will be used when + processing credit card transactions in the Point Of Sale. Setting up a Mercury + configuration will enable you to allow payments with various credit cards + (eg. Visa, MasterCard, Discovery, American Express, ...). After setting up this + configuration you should associate it with a Point Of Sale payment method. +

+ We currently support the MagTek Dynamag card reader device. It can be connected + directly to the Point Of Sale device or it can be connected to the POSBox. +

+ Using the Mercury integration in the Point Of Sale is easy: just press the + associated payment method. After that the amount can be adjusted (eg. for cashback) + just like on any other payment line. Whenever the payment line is set up, a card + can be swiped through the card reader device. +

+ For quickly handling orders: just swiping a credit card when on the payment screen + (without having pressed anything else) will charge the full amount of the order to + the card. +

+
+ + + + +
+
+
+
+ + + Mercury Configurations + pos_mercury.configuration + + + + + + + + + + Mercury Configurations + ir.actions.act_window + pos_mercury.configuration + form + tree,form + +

+ Click to configure your card reader. +

+
+
+ + + POS Journal + account.journal + + + + + + + + + + POS bank statements + account.bank.statement + + + + + + + + + + + + POS orders + pos.order + + + + + + + + + + + + + Mercury token cleaning + + + 1 + days + -1 + + + + + + + +
+