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:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ 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 @@
+
+
+
+
+
+
+
+
+ OneTime
+
+
+
+
+
+
+
+ RecordNumberRequested
+ Allow
+
+
+ Swiped
+
+
+ MagneSafe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Allow
+
+
+
+
+
+
+
+
+
+
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
+ 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
+
+
+
+
+
+
+
+
+