Merge branch 'new/12.0/pos_pax' into '12.0'

new/12.0/pos_pax into 12.0

See merge request hibou-io/hibou-odoo/suite!687
This commit is contained in:
Jared Kipe
2020-11-26 12:22:51 +00:00
13 changed files with 1275 additions and 0 deletions

3
pos_pax/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import models

43
pos_pax/__manifest__.py Normal file
View File

@@ -0,0 +1,43 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{
'name': 'POS PAX Terminal Credit Card',
'author': 'Hibou Corp. <hello@hibou.io>',
'version': '12.0.1.0.0',
'category': 'Point of Sale',
'sequence': 6,
'summary': 'PAX Terminal 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 on the PAX Terminal (no credit credit card
information through Odoo itself).
Depending on Device and processor support, this integration can handle:
* Magnetic swiped cards
* EMV chip cards
* Contactless (including Apple Pay, Samsung Pay, Google Pay)
""",
'depends': [
'web',
'pos_sale',
# 'hibou_professional',
],
'website': 'https://hibou.io',
'data': [
'views/pos_pax_templates.xml',
'views/pos_pax_views.xml',
'views/pos_config_setting_views.xml',
],
'demo': [
],
'qweb': [
'static/src/xml/pos_pax.xml',
],
'installable': True,
'auto_install': False,
}

View File

@@ -0,0 +1,4 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import pos_pax
from . import update

61
pos_pax/models/pos_pax.py Normal file
View File

@@ -0,0 +1,61 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import models, fields, api, _
from odoo.tools.float_utils import float_compare
class AccountBankStatementLine(models.Model):
_inherit = "account.bank.statement.line"
pax_card_number = fields.Char(string='Card Number', help='Masked credit card.')
pax_txn_id = fields.Char(string='PAX Transaction ID')
class AccountJournal(models.Model):
_inherit = 'account.journal'
pos_use_pax = fields.Boolean(string='Use POS PAX Terminal',
help='When used in POS, communicate with PAX Terminal for transactions.')
class PosConfig(models.Model):
_inherit = 'pos.config'
pax_endpoint = fields.Char(string='PAX Endpoint',
help='Endpoint for PAX device (include protocol (http or https) and port). '
'e.g. http://192.168.1.101:10009')
class PosOrder(models.Model):
_inherit = "pos.order"
@api.model
def _payment_fields(self, ui_paymentline):
fields = super(PosOrder, self)._payment_fields(ui_paymentline)
fields.update({
'pax_card_number': ui_paymentline.get('pax_card_number'),
'pax_txn_id': ui_paymentline.get('pax_txn_id'),
})
return fields
def add_payment(self, data):
statement_id = super(PosOrder, self).add_payment(data)
statement_lines = self.env['account.bank.statement.line'].search([('statement_id', '=', statement_id),
('pos_statement_id', '=', self.id),
('journal_id', '=', data['journal'])])
statement_lines = statement_lines.filtered(lambda line: float_compare(line.amount, data['amount'],
precision_rounding=line.journal_currency_id.rounding) == 0)
# 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.pax_card_number:
line.pax_card_number = data.get('pax_card_number')
line.pax_txn_id = data.get('pax_txn_id')
break
return statement_id

20
pos_pax/models/update.py Normal file
View File

@@ -0,0 +1,20 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import models
class PublisherWarrantyContract(models.AbstractModel):
_inherit = 'publisher_warranty.contract'
def _get_hibou_modules(self):
modules = super(PublisherWarrantyContract, self)._get_hibou_modules()
try:
self.env.cr.execute(
'SELECT COUNT(*) FROM pos_config WHERE pax_endpoint != \'\' AND pax_endpoint IS NOT NULL')
pax_count = self.env.cr.fetchone()[0] or 0
modules.update({
'pos_pax': pax_count,
})
except:
pass
return modules

View File

@@ -0,0 +1,17 @@
.pos .paymentline.selected.o_pos_pax_txn_pending, .pos .paymentline.o_pos_pax_txn_pending {
background: rgb(239, 153, 65);
}
.pos .col-tendered.edit.o_pos_pax_txn_pending {
color: rgb(239, 153, 65);
box-shadow: 0px 0px 0px 3px rgb(239, 153, 65);
}
.pos .paymentline .pax_send_transaction {
cursor: pointer !important;
border: 1px solid rgba(255, 255, 255, 0.5);
background-color: rgba(255,255,255, 0.2);
display: block;
text-align: center;
margin: 6px 0 0 0;
padding: 2px 5px;
}

133
pos_pax/static/src/js/jquery_base64.js vendored Normal file
View File

@@ -0,0 +1,133 @@
odoo.define('pos_pax.jquery_base64', function (require) {
"use strict";
// Hibou Corp. © 2020 - Wrapped library for PAX Device
var jQuery = require('jquery');
/*!
* jquery.base64.js 0.1 - https://github.com/yckart/jquery.base64.js
* Makes Base64 en & -decoding simpler as it is.
*
* Based upon: https://gist.github.com/Yaffle/1284012
*
* Copyright (c) 2012 Yannick Albert (http://yckart.com)
* Licensed under the MIT license (http://www.opensource.org/licenses/mit-license.php).
* 2013/02/10
**/
;(function ($) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
a256 = '',
r64 = [256],
r256 = [256],
i = 0;
var UTF8 = {
/**
* Encode multi-byte Unicode string into utf-8 multiple single-byte characters
* (BMP / basic multilingual plane only)
*
* Chars in range U+0080 - U+07FF are encoded in 2 chars, U+0800 - U+FFFF in 3 chars
*
* @param {String} strUni Unicode string to be encoded as UTF-8
* @returns {String} encoded string
*/
encode: function (strUni) {
// use regular expressions & String.replace callback function for better efficiency
// than procedural approaches
var strUtf = strUni.replace(/[\u0080-\u07ff]/g, // U+0080 - U+07FF => 2 bytes 110yyyyy, 10zzzzzz
function (c) {
var cc = c.charCodeAt(0);
return String.fromCharCode(0xc0 | cc >> 6, 0x80 | cc & 0x3f);
})
.replace(/[\u0800-\uffff]/g, // U+0800 - U+FFFF => 3 bytes 1110xxxx, 10yyyyyy, 10zzzzzz
function (c) {
var cc = c.charCodeAt(0);
return String.fromCharCode(0xe0 | cc >> 12, 0x80 | cc >> 6 & 0x3F, 0x80 | cc & 0x3f);
});
return strUtf;
},
/**
* Decode utf-8 encoded string back into multi-byte Unicode characters
*
* @param {String} strUtf UTF-8 string to be decoded back to Unicode
* @returns {String} decoded string
*/
decode: function (strUtf) {
// note: decode 3-byte chars first as decoded 2-byte strings could appear to be 3-byte char!
var strUni = strUtf.replace(/[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars
function (c) { // (note parentheses for precence)
var cc = ((c.charCodeAt(0) & 0x0f) << 12) | ((c.charCodeAt(1) & 0x3f) << 6) | (c.charCodeAt(2) & 0x3f);
return String.fromCharCode(cc);
})
.replace(/[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars
function (c) { // (note parentheses for precence)
var cc = (c.charCodeAt(0) & 0x1f) << 6 | c.charCodeAt(1) & 0x3f;
return String.fromCharCode(cc);
});
return strUni;
}
};
while (i < 256) {
var c = String.fromCharCode(i);
a256 += c;
r256[i] = i;
r64[i] = b64.indexOf(c);
++i;
}
function code(s, discard, alpha, beta, w1, w2) {
s = String(s);
var buffer = 0,
i = 0,
length = s.length,
result = '',
bitsInBuffer = 0;
while (i < length) {
var c = s.charCodeAt(i);
c = c < 256 ? alpha[c] : -1;
buffer = (buffer << w1) + c;
bitsInBuffer += w1;
while (bitsInBuffer >= w2) {
bitsInBuffer -= w2;
var tmp = buffer >> bitsInBuffer;
result += beta.charAt(tmp);
buffer ^= tmp << bitsInBuffer;
}
++i;
}
if (!discard && bitsInBuffer > 0) result += beta.charAt(buffer << (w2 - bitsInBuffer));
return result;
}
var Plugin = $.base64 = function (dir, input, encode) {
return input ? Plugin[dir](input, encode) : dir ? null : this;
};
Plugin.btoa = Plugin.encode = function (plain, utf8encode) {
plain = Plugin.raw === false || Plugin.utf8encode || utf8encode ? UTF8.encode(plain) : plain;
plain = code(plain, false, r256, b64, 8, 6);
return plain + '===='.slice((plain.length % 4) || 4);
};
Plugin.atob = Plugin.decode = function (coded, utf8decode) {
coded = String(coded).split('=');
var i = coded.length;
do {
--i;
coded[i] = code(coded[i], true, r64, a256, 6, 8);
} while (i > 0);
coded = coded.join('');
return Plugin.raw === false || Plugin.utf8decode || utf8decode ? UTF8.decode(coded) : coded;
};
}(jQuery));
return jQuery;
});

View File

@@ -0,0 +1,479 @@
odoo.define('pos_pax.pax_device', function (require) {
"use strict";
var $ = require('pos_pax.jquery_base64');
// Hibou Corp. © 2020 - Wrapped library for PAX Device
// Additions and Fixes:
// 2020-11-12 Fixed variable i and ii in getLRC
// 2020-11-12 Fixed/added 'fail' mechanisms for XHR
//HEX TO BASE64
function hexToBase64(str) {
return $.base64.btoa(String.fromCharCode.apply(null,
str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" "))
);
}
//BASE64 TO HEX
function base64ToHex(str) {
for (var i = 0, bin = $.base64.atob(str), hex = []; i < bin.length; ++i) {
var tmp = bin.charCodeAt(i).toString(16);
if (tmp.length === 1) tmp = "0" + tmp;
hex[hex.length] = tmp;
}
return hex.join(" ");
}
function StringToHex(response){
var responseHex = "";
for(var i=0; i<response.length; i++){
if(responseHex == "")
responseHex = response.charCodeAt(i).toString(16).length<2?'0'+response.charCodeAt(i).toString(16):response.charCodeAt(i).toString(16);
else
responseHex += response.charCodeAt(i).toString(16).length<2?" " + '0'+response.charCodeAt(i).toString(16):" " + response.charCodeAt(i).toString(16);
}
return responseHex;
}
function HexToString(response){
var responseHex = "";
var arr = response.split(" ");
for(var i=0; i<arr.length; i++){
if(arr[i] == "")
continue;
responseHex += String.fromCharCode(parseInt(arr[i],16));
}
return responseHex;
}
var PAX = {
//IP of the POS
mDestinationIP : "http://127.0.0.1:10009", // - OLD "http://192.167.2.100:10009"; //http://112.199.49.146:8181
mStx : {
hex: 0x02,
code: "02"
},
mFS : {
hex : 0x1c,
code : "1c"
},
mEtx : {
hex : 0x03,
code : "03"
},
mUS : {
hex : 0x1F,
code : "1F"
},
//var _this : this;
customData : '',
timeout : {
"Initialize":120*1000,
"GetSignature":120*1000,
"DoSignature":120*1000,
"DoCredit":120*1000
},
//Set ip and port
Settings : function(ip,port){
this.mDestinationIP = "http://" + ip + ":" + port;
console.log("New service address: "+this.mDestinationIP);
},
AjaxTimeOut : function(command,timeout){
this.timeout[command]= timeout;
},
SetCustomData : function(custom_data){
this.customData = custom_data;
console.log(custom_data);
},
//Get LRC
getLRC : function(params){
var lrc = 0;
for(var i=1; i< params.length; i++){
var type_of = typeof(params[i]);
if(type_of == "string"){
var element = params[i].split("");
for(var ii=0; ii<element.length; ii++){
lrc ^= element[ii].charCodeAt(0);
}
}else{
lrc ^= params[i];
}
}
return (lrc>0)?String.fromCharCode(lrc):0;
},
//Connect to the server
HttpCommunication : function(commandType,url,callback,timeout,fail){
var xhr = null;
if(window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
try{
xhr = new ActiveXObject('Microsoft.XMLHttp');
}catch(e){
xhr = new ActiveXObject('msxml2.xmlhttp');
}
}
//get
xhr.open("GET", url,true);
xhr.onreadystatechange=function(){
if(xhr.readyState==4) {
//alert(xhr.status);
if(xhr.status==200) {
var response = xhr.responseText;
console.log("Raw response: "+response);
var checkParams = StringToHex(response).split(" ").pop();
var RedundancyCheck = StringToHex(response).split(" ").pop().substring(1);
var check = PAX.getLRC(checkParams);
if(check == RedundancyCheck){
//get package detail info
var packetInfo = [];
var len = StringToHex(response).indexOf("03");
var hex = StringToHex(response).slice(0,len).split(/02|1c/);
console.log(hex);
if(commandType == "DoCredit"){
var subHex=[], subPacketInfo=[];
for(var i=0; i<hex.length; i++){
if(hex[i] != ""){
if(hex[i].indexOf("1f")>0){
subHex = hex[i].split("1f");
console.log(subHex);
subPacketInfo = [];
for(var j=0; j<subHex.length; j++){
if(subHex[j]!=''){
subPacketInfo.push(HexToString(subHex[j]));
}
}
console.log(subPacketInfo);
packetInfo.push(subPacketInfo);
}else{
packetInfo[i] = HexToString(hex[i]);
}
}
}
}else{
for(var i=0; i<hex.length; i++){
if(hex[i] != ""){
packetInfo[i] = HexToString(hex[i]);
}
}
}
console.log("Separate package info: ");
console.log(packetInfo);
callback(packetInfo);
}
} else {
if(fail) {
fail(xhr.status);
}
}
}
};
xhr.send(null);
},
Initialize : function(initialInfo,callback,fail){
var params = [this.mStx.hex,initialInfo.command,this.mFS.hex, initialInfo.version, this.mEtx.hex];
//[02]A08[1c]1.28[1c]0[1c]90000[03]
//var params = [0x02,"A08",0x1c,"1.28",0x1c, "0", 0x1c,"90000",0x03];
var lrc = this.getLRC(params);
var command_hex = base64ToHex($.base64.btoa(initialInfo.command));
var version_hex = base64ToHex($.base64.btoa(initialInfo.version));
//var elements = [this.mStx, command_hex, this.mFS, version_hex, this.mEtx, base64ToHex($.base64.btoa(lrc))];
var elements = [this.mStx.code, command_hex, this.mFS.code, version_hex, this.mEtx.code, base64ToHex($.base64.btoa(lrc))];
var final_string = elements.join(" ");
//console.log("final_string: " + final_string);
var final_b64 = hexToBase64(final_string);
console.log("LRC: " + lrc);
console.log("Base64: " + final_b64);
var url = this.mDestinationIP + '?' + final_b64;
console.log("URL: " + url);
this.HttpCommunication('Initialize',url,function(response){
callback(response);
},PAX.timeout.Initialize,fail);
},
//GET SIGNATURE
GetSignature : function(getSignatureInfo,callback,fail){
var params = [this.mStx.hex,getSignatureInfo.command,this.mFS.hex,getSignatureInfo.version,this.mFS.hex, getSignatureInfo.offset, this.mFS.hex,getSignatureInfo.requestlength,this.mEtx.hex];
var lrc = this.getLRC(params);
//prepare for base64 encoding.
var command_hex = base64ToHex($.base64.btoa(getSignatureInfo.command));
var version_hex = base64ToHex($.base64.btoa(getSignatureInfo.version));
var offset_hex = base64ToHex($.base64.btoa(getSignatureInfo.offset));
var requestlength_hex = base64ToHex($.base64.btoa(getSignatureInfo.requestlength));
//var elements = [this.mStx.code, command_hex, this.mFS.code, version_hex, this.mFS.code, offset_hex, this.mFS.code, requestlength_hex, this.mEtx.code, base64ToHex($.base64.btoa(lrc))];
var elements = [this.mStx.code];
elements.push(command_hex);
elements.push(this.mFS.code);
elements.push(version_hex);
elements.push(this.mFS.code);
if(offset_hex != ''){
elements.push(offset_hex);
}
elements.push(this.mFS.code);
if(requestlength_hex != ''){
elements.push(requestlength_hex);
}
elements.push(this.mEtx.code);
elements.push(base64ToHex($.base64.btoa(lrc)));
var final_string = elements.join(" ");
var final_b64 = hexToBase64(final_string);
console.log("LRC: " + lrc);
console.log("Base64: " + final_b64);
var url = this.mDestinationIP + '?' + final_b64;
console.log("URL: " + url);
this.HttpCommunication('GetSignature',url,function(response){
callback(response);
},PAX.timeout.GetSignature,fail);
},
//DO SIGNATURE
DoSignature : function(doSignatureInfo,callback,fail){
var params = [this.mStx.hex,doSignatureInfo.command, this.mFS.hex, doSignatureInfo.version, this.mFS.hex, doSignatureInfo.uploadFlag, this.mFS.hex,doSignatureInfo.hostReferenceNumber, this.mFS.hex, doSignatureInfo.edcType, this.mFS.hex, doSignatureInfo.timeout, this.mEtx.hex];
var lrc = this.getLRC(params);
//prepare for base64 encoding.
var command_hex = base64ToHex($.base64.btoa(doSignatureInfo.command));
var version_hex = base64ToHex($.base64.btoa(doSignatureInfo.version));
var uploadFlag_hex = base64ToHex($.base64.btoa(doSignatureInfo.uploadFlag));
var hostReferenceNumber_hex = base64ToHex($.base64.btoa(doSignatureInfo.hostReferenceNumber));
var edcType_hex = base64ToHex($.base64.btoa(doSignatureInfo.edcType));
var timeout_hex = base64ToHex($.base64.btoa(doSignatureInfo.timeout));
var elements = [this.mStx.code];
elements.push(command_hex);
elements.push(this.mFS.code);
elements.push(version_hex);
elements.push(this.mFS.code);
if(uploadFlag_hex != ''){
elements.push(uploadFlag_hex);
}
elements.push(this.mFS.code);
if(hostReferenceNumber_hex != ''){
elements.push(hostReferenceNumber_hex);
}
elements.push(this.mFS.code);
if(edcType_hex != ''){
elements.push(edcType_hex);
}
elements.push(this.mFS.code);
if(timeout_hex != ''){
elements.push(timeout_hex);
}
elements.push(this.mEtx.code);
elements.push(base64ToHex($.base64.btoa(lrc)));
var final_string = elements.join(" ");
var final_b64 = hexToBase64(final_string);
console.log("LRC: " + lrc);
console.log("Base64: " + final_b64);
var url = this.mDestinationIP + '?' + final_b64;
console.log("URL: " + url);
this.HttpCommunication('DoSignature',url,function(response){
callback(response);
},PAX.timeout.DoSignature,fail);
},
PushParams : function(params,type,objectInfo){
var empty = 0;
var arr = [];
arr = arr.concat(params);
for(name in objectInfo){
if(objectInfo[name] == '' && type!="additionalInformation")
{
arr.push(this.mUS.hex);
continue;
}
if(type == "additionalInformation"){
if(objectInfo[name] == ''){
continue;
}
empty++;
arr.push(name+"="+objectInfo[name].toString());
}else{
empty++;
arr.push(objectInfo[name].toString());
}
arr.push(this.mUS.hex);
}
arr.pop();
if(empty==0 && type!="additionalInformation"){
arr = params;
}
if(empty==0 && type=="additionalInformation"){
arr.push(this.mFS.hex);
}
//console.log(params);
return arr;
},
AddBase64 : function(elements,type,objectInfo){
//console.log(objectInfo);
var empty = 0;
var arr = [];
arr = arr.concat(elements);
for(name in objectInfo){
if(objectInfo[name] == '' && type!="additionalInformation")
{
arr.push(this.mUS.code);
continue;
}
if(type == "additionalInformation"){
if(objectInfo[name] == '')
continue;
empty++;
arr.push(base64ToHex($.base64.btoa(name+"="+objectInfo[name].toString())));
}else{
empty++;
arr.push(base64ToHex($.base64.btoa(objectInfo[name].toString())));
}
arr.push(this.mUS.code);
}
arr.pop();
if(empty==0 && type!="additionalInformation"){
arr = elements;
}
if(empty==0 && type=="additionalInformation"){
arr.push(this.mFS.code);
}
//console.log(arr);
return arr;
},
//DO Credit
DoCredit : function(doCreditInfo,callback,fail){
var amountInformation,accountInformation,traceInformation,avsInformation,cashierInformation,commercialInformation,motoEcommerce,additionalInformation;
var params = [this.mStx.hex,doCreditInfo.command, this.mFS.hex, doCreditInfo.version];
params.push(this.mFS.hex);
if(doCreditInfo.transactionType != ''){
params.push(doCreditInfo.transactionType);
}
params.push(this.mFS.hex);
params = this.PushParams(params,"amountInformation",doCreditInfo.amountInformation);
params.push(this.mFS.hex);
params = this.PushParams(params,"accountInformation",doCreditInfo.accountInformation);
params.push(this.mFS.hex);
params = this.PushParams(params,"traceInformation",doCreditInfo.traceInformation);
params.push(this.mFS.hex);
params = this.PushParams(params,"avsInformation",doCreditInfo.avsInformation);
params.push(this.mFS.hex);
params = this.PushParams(params,"cashierInformation",doCreditInfo.cashierInformation);
params.push(this.mFS.hex);
params = this.PushParams(params,"commercialInformation",doCreditInfo.commercialInformation);
params.push(this.mFS.hex);
params = this.PushParams(params,"motoEcommerce",doCreditInfo.motoEcommerce);
params.push(this.mFS.hex);
params = this.PushParams(params,"additionalInformation",doCreditInfo.additionalInformation);
params.push(this.mEtx.hex);
var lrc = this.getLRC(params);
console.log(params);
//prepare for base64 encoding.
var command_hex = base64ToHex($.base64.btoa(doCreditInfo.command));
var version_hex = base64ToHex($.base64.btoa(doCreditInfo.version));
var transactionType_hex = base64ToHex($.base64.btoa(doCreditInfo.transactionType));
var amountInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.amountInformation));
var accountInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.accountInformation));
var traceInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.traceInformation));
var avsInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.avsInformation));
var cashierInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.cashierInformation));
var commercialInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.commercialInformation));
var motoEcommerce_hex = base64ToHex($.base64.btoa(doCreditInfo.motoEcommerce));
var additionalInformation_hex = base64ToHex($.base64.btoa(doCreditInfo.additionalInformation));
//var elements = [this.mStx.code, command_hex, this.mFS.code, version_hex, this.mFS.code, uploadFlag_hex, this.mFS.code, timeout, this.mEtx.code, base64ToHex($.base64.btoa(lrc))];
var elements = [this.mStx.code];
elements.push(command_hex);
elements.push(this.mFS.code);
elements.push(version_hex);
elements.push(this.mFS.code);
if(transactionType_hex != ''){
elements.push(transactionType_hex);
}
elements.push(this.mFS.code);
elements = this.AddBase64(elements,"amountInformation",doCreditInfo.amountInformation);
elements.push(this.mFS.code);
elements = this.AddBase64(elements,"accountInformation",doCreditInfo.accountInformation);
elements.push(this.mFS.code);
elements = this.AddBase64(elements,"traceInformation",doCreditInfo.traceInformation);
elements.push(this.mFS.code);
elements = this.AddBase64(elements,"avsInformation",doCreditInfo.avsInformation);
elements.push(this.mFS.code);
elements = this.AddBase64(elements,"cashierInformation",doCreditInfo.cashierInformation);
elements.push(this.mFS.code);
elements = this.AddBase64(elements,"commercialInformation",doCreditInfo.commercialInformation);
elements.push(this.mFS.code);
elements = this.AddBase64(elements,"motoEcommerce",doCreditInfo.motoEcommerce);
elements.push(this.mFS.code);
elements = this.AddBase64(elements,"additionalInformation",doCreditInfo.additionalInformation);
elements.push(this.mEtx.code);
elements.push(base64ToHex($.base64.btoa(lrc)));
console.log("elements");
console.log(elements);
var final_string = elements.join(" ");
var final_b64 = hexToBase64(final_string);
console.log("LRC: " + lrc);
console.log("Base64: " + final_b64);
// if(customData != ''){
// final_b64 = hexToBase64(final_string+"&custom_data=<PAX>"+customData+"</PAX>");
// }
var url = this.mDestinationIP + '?' + final_b64;
console.log("URL: " + url);
this.HttpCommunication('DoCredit',url,function(response){
callback(response);
},PAX.timeout.DoCredit,fail);
}
};
return PAX;
});

View File

@@ -0,0 +1,349 @@
odoo.define('pos_pax.pos_pax', function (require) {
"use strict";
// Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
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 _t = core._t;
var PopupWidget = require('point_of_sale.popups');
var PaymentScreenWidget = screens.PaymentScreenWidget;
var PAX = require('pos_pax.pax_device');
PAX.mDestinationIP = '';
pos_model.load_fields("account.journal", "pos_use_pax");
pos_model.PosModel = pos_model.PosModel.extend({
getPAXOnlinePaymentJournals: function () {
var self = this;
var online_payment_journals = [];
$.each(this.journals, function (i, val) {
if (val.pos_use_pax) {
online_payment_journals.push({label:self.getCashRegisterByJournalID(val.id).journal_id[1], item:val.id});
}
});
return online_payment_journals;
},
getCashRegisterByJournalID: function (journal_id) {
var cashregister_return = false;
$.each(this.cashregisters, function (index, cashregister) {
if (cashregister.journal_id[0] === journal_id) {
cashregister_return = cashregister;
}
});
return cashregister_return;
},
decodePAXResponse: function (data) {
console.log(data);
if (data[5] == 'ABORTED') {
console.log('aborted');
return {fail: 'Transaction Aborted'};
} else if (data[5] == 'OK') {
return {
success: true,
approval: data[6][2],
txn_id: data[10][0],
card_num: '***' + data[9][0],
}
}
return {fail: 'Unknown Response. ' + data};
}
});
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 = true;
this.pax_txn_pending = json.pax_txn_pending;
this.pax_card_number = json.pax_card_number;
this.pax_approval = json.pax_approval;
this.pax_txn_id = json.pax_txn_id;
this.set_credit_card_name();
},
export_as_JSON: function () {
return _.extend(_paylineproto.export_as_JSON.apply(this, arguments), {
paid: this.paid,
pax_txn_pending: this.pax_txn_pending,
pax_card_number: this.pax_card_number,
pax_approval: this.pax_approval,
pax_txn_id: this.pax_txn_id,
});
},
set_credit_card_name: function () {
if (this.pax_card_number) {
this.name = this.pax_card_number;
}
}
});
// Popup to show all transaction state for the payment.
var PAXPaymentTransactionPopupWidget = PopupWidget.extend({
template: 'PAXPaymentTransactionPopupWidget',
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:'pax-payment-transaction', widget: PAXPaymentTransactionPopupWidget});
PaymentScreenWidget.include({
_get_pax_txn_pending_line: function () {
var i = 0;
var lines = this.pos.get_order().get_paymentlines();
for (i = 0; i < lines.length; i++) {
if (lines[i].pax_txn_pending) {
return lines[i];
}
}
return 0;
},
// Handler to manage the card reader string
pax_credit_transaction: function (old_deferred, retry_nr) {
var order = this.pos.get_order();
if(this.pos.getPAXOnlinePaymentJournals().length === 0) {
return;
}
var self = this;
var transaction = {};
var pending_line = self._get_pax_txn_pending_line();
if ( ! pending_line ) {
console.log('no pending line found!');
return;
}
var purchase_amount = pending_line.get_amount();
var transactionType = '01'; // SALE
if (purchase_amount < 0.0) {
purchase_amount = -purchase_amount;
transactionType = '02'; // RETURN
}
transaction = {
command: 'T00',
version: '1.28',
transactionType: transactionType,
amountInformation: {
TransactionAmount: (purchase_amount * 100) | 0, // cast to integer
},
cashierInformation: {
ClerkID: this.pos.user.name,
},
traceInformation: {
ReferenceNumber: self.pos.get_order().uid,
// InvoiceNumber: self.pos.get_order().uid,
},
}
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('pax-payment-transaction', {
transaction: def
});
def.notify({
message: _t('Handling transaction...'),
});
}
PAX.mDestinationIP = this.pos.config.pax_endpoint;
PAX.DoCredit(transaction, function (response) {
console.log(response);
var parsed_response = self.pos.decodePAXResponse(response);
if (parsed_response.fail) {
def.resolve({message: parsed_response.fail})
return;
}
if (parsed_response.success) {
pending_line.paid = true;
pending_line.pax_card_number = parsed_response.card_num;
pending_line.pax_txn_id = parsed_response.txn_id;
pending_line.pax_approval = parsed_response.approval;
pending_line.pax_txn_pending = false;
pending_line.set_credit_card_name();
self.order_changes();
self.reset_input();
self.render_paymentlines();
order.trigger('change', order);
def.resolve({message: 'Approval ' + parsed_response.approval, auto_close: true})
}
def.resolve({message: _t('Unknown response.')})
}, function (fail){
def.resolve({message: _t('Communication Failure: ') + fail});
});
},
remove_paymentline_by_ref: function (line) {
this.pos.get_order().remove_paymentline(line);
this.reset_input();
this.render_paymentlines();
},
pax_do_reversal: function (line) {
var def = new $.Deferred();
var self = this;
var transaction = {};
// show the transaction popup.
// the transaction deferred is used to update transaction status
this.gui.show_popup('pax-payment-transaction', {
transaction: def
});
def.notify({
message: 'Sending reversal...',
});
transaction = {
command: 'T00',
version: '1.28',
transactionType: (line.get_amount() > 0) ? '17' : '18', // V/SALE, V/RETURN
cashierInformation: {
ClerkID: this.pos.user.name,
},
traceInformation: {
ReferenceNumber: self.pos.get_order().uid,
InvoiceNumber: '',
AuthCode: line.pax_approval,
TransactionNumber: line.pax_txn_id,
},
}
PAX.mDestinationIP = this.pos.config.pax_endpoint;
PAX.DoCredit(transaction, function (response) {
var parsed_response = self.pos.decodePAXResponse(response);
if (parsed_response.fail) {
def.resolve({message: parsed_response.fail})
return;
}
if (parsed_response.success) {
def.resolve({
message: _t('Reversal succeeded.'),
auto_close: true,
});
self.remove_paymentline_by_ref(line);
return;
}
def.resolve({message: _t('Unknown response.')})
}, function (fail){
def.resolve({message: _t('Communication Failure: ') + fail});
});
},
click_delete_paymentline: function (cid) {
var lines = this.pos.get_order().get_paymentlines();
for (var i = 0; i < lines.length; i++) {
if (lines[i].cid === cid && lines[i].pax_txn_id) {
this.pax_do_reversal(lines[i]);
return;
}
}
this._super(cid);
},
// make sure there is only one paymentline waiting for a swipe
click_paymentmethods: function (id) {
var order = this.pos.get_order();
var cashregister = null;
for (var 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.pos_use_pax) {
var pending_line = this._get_pax_txn_pending_line();
if (pending_line) {
this.gui.show_popup('error',{
'title': _t('Error'),
'body': _t('One credit card txn already pending.'),
});
} else {
this._super(id);
order.selected_paymentline.pax_txn_pending = true;
this.render_paymentlines();
order.trigger('change', order); // needed so that export_to_JSON gets triggered
}
} else {
this._super(id);
}
},
click_pax_send_transaction: function (id) {
var pending_txn_line = this._get_pax_txn_pending_line();
if (!pending_txn_line) {
this.gui.show_popup('error',{
'title': _t('Error'),
'body': _t('No pending payment line to send.'),
});
return;
}
this.pax_credit_transaction();
},
render_paymentlines: function() {
this._super();
var self = this;
self.$('.paymentlines-container').on('click', '.pax_send_transaction', function(){
self.click_pax_send_transaction();
});
},
// before validating, get rid of any paymentlines that are pending
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].pax_txn_pending) {
this.pos.get_order().remove_paymentline(lines[i]);
this.render_paymentlines();
}
}
}
this._super(force_validation);
},
});
});

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" inherit_id="point_of_sale.template">
<t t-name="PAXPaymentTransactionPopupWidget">
<div class="modal-dialog">
<div class="popup">
<p class="title">PAX 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.pax_txn_pending">
<div>WAITING FOR TXN</div>
</t>
<t t-if="! line.pax_txn_pending">
<t t-esc='line.name' />
</t>
<t t-else="">
<span class="btn btn-small pax_send_transaction">Send</span>
</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.pax_txn_pending ? \'o_pos_pax_txn_pending\' : \'\'}');
</t>
<t t-jquery="tbody tr.paymentline[t-att-data-cid*='line.cid']">
this.removeAttr('class');
this.attr('t-attf-class', 'paymentline #{line.pax_txn_pending ? \'o_pos_pax_txn_pending\' : \'\'}');
</t>
<t t-jquery="tbody tr td.col-tendered.edit">
this.removeAttr('class');
this.attr('t-attf-class', 'col-tendered edit #{line.pax_txn_pending ? \'o_pos_pax_txn_pending\' : \'\'}');
</t>
</t>
<t t-name="PosPAXSignature">
<t t-foreach="paymentlines" t-as="paymentline">
<t t-if="!gift &amp;&amp; paymentline.pax_txn_id &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-if="!gift &amp;&amp; line.pax_auth_code">
<line line-ratio="1">
<left><pre> APPROVAL CODE:</pre><t t-esc="line.pax_approval"/></left>
</line>
</t>
</t>
<t t-jquery="receipt" t-operation="append">
<div>
<t t-call="PosPAXSignature"/>
</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="!gift &amp;&amp; line.pax_auth_code">
&amp;nbsp;&amp;nbsp;APPROVAL CODE: <t t-esc="line.pax_approval"/>
</t>
</td>
</tr>
</t>
<t t-jquery="t[t-if*='receipt.footer']" t-operation="after">
<div class="pos-center-align">
<t t-call="PosPAXSignature"/>
</div>
</t>
</t>
</templates>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pos_config_view_form_inherit_pos_pax" model="ir.ui.view">
<field name="name">pos.config.form.inherit.pax</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='payment_methods']" position="after">
<div class="col-12 col-lg-6 o_setting_box" id="pos_pax">
<div class="o_setting_right_pane">
<strong class="o_form_label">PAX</strong>
<div>
<p>Your PAX Device Endpoint</p>
<field name="pax_endpoint"/>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets" inherit_id="point_of_sale.assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/pos_pax/static/src/js/jquery_base64.js"></script>
<script type="text/javascript" src="/pos_pax/static/src/js/pax_device.js"></script>
<script type="text/javascript" src="/pos_pax/static/src/js/pos_pax.js"></script>
<link rel="stylesheet" href="/pos_pax/static/src/css/pos_pax.css" />
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0"?>
<odoo>
<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="//group[@name='amount_authorized']" position="after">
<group attrs="{'invisible': [('type', '!=', 'bank')]}">
<field name="pos_use_pax"/>
</group>
</xpath>
</field>
</record>
<record id="view_account_bank_journal_form_inherited_pos_pax" model="ir.ui.view">
<field name="name">account.bank.journal.form.inherited.pos.pax</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="point_of_sale.view_account_bank_journal_form_inherited_pos"></field>
<field name="arch" type="xml">
<xpath expr="//field[@name='journal_user']" position="after">
<field name="pos_use_pax" attrs="{'invisible': [('journal_user', '=', False)]}"/>
</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="pax_card_number"/>
<field name="pax_txn_id"/>
</xpath>
</field>
</record>
</odoo>