[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:
committed by
Frédéric van der Essen
parent
3a26daeb29
commit
04fb3dfcff
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import models
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
@@ -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 && ! 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">
|
||||
&nbsp;&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>
|
||||
Reference in New Issue
Block a user