[IMP] pos_mercury: pos extension for electronic card payment.

This new module integrate the 'Mercury Pay' electronic payment
provider with the point of sale.

This commit is the end result of a large branch whose main
author is Joren Van Onder <jov@odoo.com>, with additional
contributions from Martin Donies <mdo@odoo.com> and Frederic van
der Essen <fva@odoo.com>
This commit is contained in:
Joren Van Onder
2015-07-02 16:42:36 +02:00
committed by Frédéric van der Essen
parent 3a26daeb29
commit 04fb3dfcff
20 changed files with 1362 additions and 72 deletions
+11 -9
View File
@@ -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),
+39 -40
View File
@@ -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();
+1 -1
View File
@@ -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.
+12 -5
View File
@@ -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) );
}
@@ -243,4 +243,5 @@ var OrderImportPopupWidget = PopupWidget.extend({
});
gui.define_popup({name:'orderimport', widget: OrderImportPopupWidget});
return PopupWidget;
});
+34 -17
View File
@@ -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,
};
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import models
+42
View File
@@ -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,
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="barcode_rule_credit" model="barcode.rule">
<field name="name">Magnetic Credit Card</field>
<field name="barcode_nomenclature_id" ref="barcodes.default_barcode_nomenclature"/>
<field name="sequence">85</field>
<field name="type">credit</field>
<field name="encoding">any</field>
<field name="pattern">%.*</field>
</record>
</data>
</openerp>
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Mercury Production Test Account (e2e + token) -->
<!-- This is a test account for testing with test cards and cannot be used in a live environment -->
<record id="pos_mercury_configuration" model="pos_mercury.configuration">
<field name="name">Mercury Demo</field>
<field name="merchant_id">334160</field>
<field name="merchant_pwd">81303DUR</field>
</record>
</data>
</openerp>
+5
View File
@@ -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
+95
View File
@@ -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
@@ -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 = '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:mer="http://www.mercurypay.com"><soapenv:Header/><soapenv:Body><mer:CreditTransaction><mer:tran>'
soap_footer = '</mer:tran><mer:pw>' + data['merchant_pwd'] + '</mer:pw></mer:CreditTransaction></soapenv:Body></soapenv:Envelope>'
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 = ""
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_pos_mercury_configuration mercury.configuration model_pos_mercury_configuration point_of_sale.group_pos_manager 1 1 1 1
3 access_pos_mercury_mercury_transaction mercury.transaction model_pos_mercury_mercury_transaction 1 0 0 0
@@ -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);
}
@@ -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('<div class="footer"><div class="button cancel">Ok</div></div>');
}
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) + "...<br/><br/>" + 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] + "<br/>" + 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 + ":<br/>" + 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 + ":<br/>" + 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();
}
});
});
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" inherit_id="point_of_sale.template">
<t t-name="PaymentTransactionPopupWidget">
<div class="modal-dialog">
<div class="popup">
<p class="title">Electronic Payment</p>
<p class="body"></p>
</div>
</div>
</t>
<t t-extend="PaymentScreen-Paymentlines">
<t t-jquery=".col-name" t-operation="inner">
<t t-if="line.cashregister.journal.type === 'bank'">
<t t-if="line.mercury_swipe_pending">
<div>WAITING FOR SWIPE</div>
</t>
<t t-if="! line.mercury_swipe_pending">
<t t-esc='line.name' />
</t>
</t>
<t t-if="line.cashregister.journal.type !== 'bank'">
<t t-esc='line.name' />
</t>
</t>
<t t-jquery="tbody tr.paymentline.selected">
this.removeAttr("class");
this.attr('t-attf-class', 'paymentline selected #{line.mercury_swipe_pending ? "o_pos_mercury_swipe_pending" : ""}');
</t>
<t t-jquery="tbody tr.paymentline[t-att-data-cid*='line.cid']">
this.removeAttr("class");
this.attr('t-attf-class', 'paymentline #{line.mercury_swipe_pending ? "o_pos_mercury_swipe_pending" : ""}');
</t>
<t t-jquery="tbody tr td.col-tendered.edit">
this.removeAttr("class");
this.attr('t-attf-class', 'col-tendered edit #{line.mercury_swipe_pending ? "o_pos_mercury_swipe_pending" : ""}');
</t>
</t>
<t t-name="PosMercurySignature">
<t t-foreach="paymentlines" t-as="paymentline">
<t t-if="paymentline.mercury_data &amp;&amp; ! printed_signature">
<br />
<div>CARDHOLDER WILL PAY CARD ISSUER</div>
<div>ABOVE AMOUNT PURSUANT</div>
<div>TO CARDHOLDER AGREEMENT</div>
<br />
<br />
<div>X______________________________</div>
<t t-set="printed_signature" t-value="true"/>
</t>
</t>
</t>
<t t-extend="XmlReceipt">
<t t-jquery="t[t-foreach*='paymentlines'][t-as*='line']" t-operation="append">
<t t-debug="true"/>
<t t-if="line.mercury_data">
<line line-ratio="1">
<left><pre> APPROVAL CODE:</pre><t t-esc="line.mercury_auth_code"/></left>
</line>
</t>
</t>
<t t-jquery="receipt" t-operation="append">
<div>
<t t-call="PosMercurySignature"/>
</div>
</t>
</t>
<t t-extend="PosTicket">
<t t-jquery="t[t-foreach*='paymentlines'][t-as*='line']" t-operation="append">
<tr>
<td colspan="2">
<t t-if="line.mercury_data">
&amp;nbsp;&amp;nbsp;APPROVAL CODE: <t t-esc="line.mercury_auth_code"/>
</t>
</td>
</tr>
</t>
<t t-jquery="t[t-if*='receipt.footer']" t-operation="after">
<div class="pos-center-align">
<t t-call="PosMercurySignature"/>
</div>
</t>
</t>
</templates>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="assets" inherit_id="point_of_sale.assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/pos_mercury/static/src/js/pos_mercury.js"></script>
<link rel="stylesheet" href="/pos_mercury/static/src/css/pos_mercury.css" />
</xpath>
</template>
</data>
</openerp>
@@ -0,0 +1,69 @@
<?xml version='1.0' encoding='utf-8'?>
<openerp>
<data>
<template id='mercury_common'>
<MerchantID><t t-esc='merchant_id'/></MerchantID>
<TranType><t t-esc='transaction_type'/></TranType>
<TranCode><t t-esc='transaction_code'/></TranCode>
<InvoiceNo><t t-esc='invoice_no'/></InvoiceNo>
<Frequency>OneTime</Frequency>
</template>
<template id='mercury_transaction'>
<TStream>
<Transaction>
<t t-call='pos_mercury.mercury_common'/>
<OperatorID><t t-esc='operator_id'/></OperatorID>
<RecordNo>RecordNumberRequested</RecordNo>
<PartialAuth>Allow</PartialAuth>
<Memo><t t-esc='memo'/></Memo>
<Account>
<AccountSource>Swiped</AccountSource>
<EncryptedBlock><t t-esc='encrypted_block'/></EncryptedBlock>
<EncryptedKey><t t-esc='encrypted_key'/></EncryptedKey>
<EncryptedFormat>MagneSafe</EncryptedFormat>
</Account>
<Amount>
<Purchase><t t-esc='"%.2f" % purchase'/></Purchase> <!-- format required by Mercury -->
</Amount>
</Transaction>
</TStream>
</template>
<template id='mercury_voidsale'>
<TStream>
<Transaction>
<t t-call='pos_mercury.mercury_common'/>
<RefNo><t t-esc='ref_no'/></RefNo>
<RecordNo><t t-esc='record_no'/></RecordNo>
<Memo><t t-esc='memo'/></Memo>
<Amount>
<Purchase><t t-esc='"%.2f" % authorize'/></Purchase> <!-- format required by Mercury -->
</Amount>
<TranInfo>
<AuthCode><t t-esc='auth_code'/></AuthCode>
<t t-if='not is_voidsale'>
<AcqRefData><t t-esc='acq_ref_data'/></AcqRefData>
<ProcessData><t t-esc='process_data'/></ProcessData>
</t>
</TranInfo>
</Transaction>
</TStream>
</template>
<template id='mercury_return'>
<TStream>
<Transaction>
<t t-call='pos_mercury.mercury_common'/>
<OperatorID><t t-esc='operator_id'/></OperatorID>
<PartialAuth>Allow</PartialAuth>
<RefNo><t t-esc='ref_no'/></RefNo>
<RecordNo><t t-esc='record_no'/></RecordNo>
<Amount>
<Purchase><t t-esc='"%.2f" % purchase'/></Purchase>
</Amount>
</Transaction>
</TStream>
</template>
</data>
</openerp>
@@ -0,0 +1,122 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="view_pos_mercury_configuration_form" model="ir.ui.view" >
<field name="name">Mercury Configurations</field>
<field name="model">pos_mercury.configuration</field>
<field name="arch" type="xml">
<form string="Card Reader">
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
</div>
<div>
<p>
<i>Mercury Configurations</i> 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.
</p><p>
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.
</p><p>
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.
</p><p>
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.
</p>
</div>
<group col="2">
<field name="merchant_id"/>
<field name="merchant_pwd"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_pos_mercury_configuration_tree" model="ir.ui.view">
<field name="name">Mercury Configurations</field>
<field name="model">pos_mercury.configuration</field>
<field name="arch" type="xml">
<tree string="Card Reader">
<field name="name"/>
<field name="merchant_id"/>
</tree>
</field>
</record>
<record id="action_configuration_form" model="ir.actions.act_window">
<field name="name">Mercury Configurations</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">pos_mercury.configuration</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to configure your card reader.
</p>
</field>
</record>
<record id="view_account_journal_pos_user_form" model="ir.ui.view">
<field name="name">POS Journal</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="point_of_sale.view_account_journal_pos_user_form"></field>
<field name="arch" type="xml">
<xpath expr="//field[@name='amount_authorized_diff']" position="after">
<field name="pos_mercury_config_id"/>
</xpath>
</field>
</record>
<record id="view_bank_statements_pos_tree" model="ir.ui.view">
<field name="name">POS bank statements</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='amount']" position="before">
<field name="mercury_prefixed_card_number"/>
<field name="mercury_card_brand"/>
<field name="mercury_card_owner_name"/>
</xpath>
</field>
</record>
<record id="view_pos_order" model="ir.ui.view">
<field name="name">POS orders</field>
<field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='amount']" position="before">
<field name="mercury_prefixed_card_number"/>
<field name="mercury_card_brand"/>
<field name="mercury_card_owner_name"/>
</xpath>
</field>
</record>
<!-- Cron job to delete mercury tokens older than 6 months -->
<record forcecreate="True" id="ir_cron_mercury_cleanup" model="ir.cron">
<field name="name">Mercury token cleaning</field>
<field eval="True" name="active"/>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
<field eval="'pos_mercury.mercury_transaction'" name="model"/>
<field eval="'cleanup_old_tokens'" name="function"/>
<field eval="'()'" name="args"/>
</record>
<menuitem parent="point_of_sale.menu_point_config_product" action="pos_mercury.action_configuration_form" id="menu_pos_pos_mercury_config"
sequence="35"/>
</data>
</openerp>