[REF] pos_mercury: migration to OWL framework

This commit is contained in:
Joseph Caburnay
2020-05-12 12:48:58 +00:00
committed by Pierre Masereel
parent 42b85f1115
commit ee593a9167
12 changed files with 759 additions and 587 deletions
+3 -1
View File
@@ -34,7 +34,9 @@ following:
'data/pos_mercury_demo.xml',
],
'qweb': [
'static/src/xml/pos_mercury.xml',
'static/src/xml/OrderReceipt.xml',
'static/src/xml/PaymentScreenPaymentLines.xml',
'static/src/xml/PaymentTransactionPopup.xml',
],
'installable': True,
'auto_install': False,
@@ -0,0 +1,24 @@
odoo.define('pos_mercury.OrderReceipt', function(require) {
'use strict';
const OrderReceipt = require('point_of_sale.OrderReceipt');
const Registries = require('point_of_sale.Registries');
const PosMercuryOrderReceipt = OrderReceipt =>
class extends OrderReceipt {
/**
* The receipt has signature if one of the paymentlines
* is paid with mercury.
*/
get hasPosMercurySignature() {
for (let line of this.paymentlines) {
if (line.mercury_data) return true;
}
return false;
}
};
Registries.Component.extend(OrderReceipt, PosMercuryOrderReceipt);
return OrderReceipt;
});
@@ -0,0 +1,569 @@
odoo.define('pos_mercury.PaymentScreen', function (require) {
'use strict';
const { _t } = require('web.core');
const PaymentScreen = require('point_of_sale.PaymentScreen');
const Registries = require('point_of_sale.Registries');
const NumberBuffer = require('point_of_sale.NumberBuffer');
const { useBarcodeReader } = require('point_of_sale.custom_hooks');
// Lookup table to store status and error messages
const lookUpCodeTransaction = {
Approved: {
'000000': _t('Transaction approved'),
},
TimeoutError: {
'001006': 'Global API Not Initialized',
'001007': 'Timeout on Response',
'003003': 'Socket Error sending request',
'003004': 'Socket already open or in use',
'003005': 'Socket Creation Failed',
'003006': 'Socket Connection Failed',
'003007': 'Connection Lost',
'003008': 'TCP/IP Failed to Initialize',
'003010': 'Time Out waiting for server response',
'003011': 'Connect Canceled',
'003053': 'Initialize Failed',
'009999': 'Unknown Error',
},
FatalError: {
'-1': 'Timeout error',
'001001': 'General Failure',
'001003': 'Invalid Command Format',
'001004': 'Insufficient Fields',
'001011': 'Empty Command String',
'002000': 'Password Verified',
'002001': 'Queue Full',
'002002': 'Password Failed – Disconnecting',
'002003': 'System Going Offline',
'002004': 'Disconnecting Socket',
'002006': 'Refused ‘Max Connections’',
'002008': 'Duplicate Serial Number Detected',
'002009': 'Password Failed (Client / Server)',
'002010': 'Password failed (Challenge / Response)',
'002011': 'Internal Server Error – Call Provider',
'003002': 'In Process with server',
'003009': 'Control failed to find branded serial (password lookup failed)',
'003012': '128 bit CryptoAPI failed',
'003014': 'Threaded Auth Started Expect Response',
'003017': 'Failed to start Event Thread.',
'003050': 'XML Parse Error',
'003051': 'All Connections Failed',
'003052': 'Server Login Failed',
'004001': 'Global Response Length Error (Too Short)',
'004002': 'Unable to Parse Response from Global (Indistinguishable)',
'004003': 'Global String Error',
'004004': 'Weak Encryption Request Not Supported',
'004005': 'Clear Text Request Not Supported',
'004010': 'Unrecognized Request Format',
'004011': 'Error Occurred While Decrypting Request',
'004017': 'Invalid Check Digit',
'004018': 'Merchant ID Missing',
'004019': 'TStream Type Missing',
'004020': 'Could Not Encrypt Response- Call Provider',
'100201': 'Invalid Transaction Type',
'100202': 'Invalid Operator ID',
'100203': 'Invalid Memo',
'100204': 'Invalid Account Number',
'100205': 'Invalid Expiration Date',
'100206': 'Invalid Authorization Code',
'100207': 'Invalid Authorization Code',
'100208': 'Invalid Authorization Amount',
'100209': 'Invalid Cash Back Amount',
'100210': 'Invalid Gratuity Amount',
'100211': 'Invalid Purchase Amount',
'100212': 'Invalid Magnetic Stripe Data',
'100213': 'Invalid PIN Block Data',
'100214': 'Invalid Derived Key Data',
'100215': 'Invalid State Code',
'100216': 'Invalid Date of Birth',
'100217': 'Invalid Check Type',
'100218': 'Invalid Routing Number',
'100219': 'Invalid TranCode',
'100220': 'Invalid Merchant ID',
'100221': 'Invalid TStream Type',
'100222': 'Invalid Batch Number',
'100223': 'Invalid Batch Item Count',
'100224': 'Invalid MICR Input Type',
'100225': 'Invalid Driver’s License',
'100226': 'Invalid Sequence Number',
'100227': 'Invalid Pass Data',
'100228': 'Invalid Card Type',
},
};
const PosMercuryPaymentScreen = (PaymentScreen) =>
class extends PaymentScreen {
constructor() {
super(...arguments);
if (this.env.pos.getOnlinePaymentMethods().length !== 0) {
useBarcodeReader({
credit: this.credit_code_action,
});
}
// How long we wait for the odoo server to deliver the response of
// a Vantiv transaction
this.server_timeout_in_ms = 95000;
// How many Vantiv transactions we send without receiving a
// response
this.server_retries = 3;
}
_get_swipe_pending_line() {
var i = 0;
var lines = this.env.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(amount, card_number, card_brand, card_owner_name) {
var i = 0;
var lines = this.env.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(
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 = self.env._t('No response from Vantiv (Vantiv down?)');
} else {
message = self.env._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(parsed_result, old_deferred, retry_nr) {
var order = this.env.pos.get_order();
if (order.get_due(order.selected_paymentline) < 0) {
this.showPopup('ErrorPopup', {
title: this.env._t('Refunds not supported'),
body: this.env._t(
"Credit card refunds are not supported. Instead select your credit card payment method, click 'Validate' and refund the original charge manually through the Vantiv backend."
),
});
return;
}
if (this.env.pos.getOnlinePaymentMethods().length === 0) {
return;
}
var self = this;
var decodedMagtek = self.env.pos.decodeMagtek(parsed_result.code);
if (!decodedMagtek) {
this.showPopup('ErrorPopup', {
title: this.env._t('Could not read card'),
body: this.env._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.env.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.env.pos.get_order().uid.replace(/-/g, ''),
purchase: purchase_amount,
payment_method_id: parsed_result.payment_method_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.showPopup('PaymentTransactionPopup', {
transaction: def,
});
def.notify({
message: this.env._t('Handling transaction...'),
});
}
this.rpc(
{
model: 'pos_mercury.mercury_transaction',
method: 'do_payment',
args: [transaction],
},
{
timeout: self.server_timeout_in_ms,
}
)
.then(function (data) {
// if not receiving a response from Vantiv, 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: self.env._t('Please setup your Vantiv merchant account.'),
});
return;
}
if (data === 'internal error') {
def.resolve({
message: self.env._t('Odoo error while processing transaction.'),
});
return;
}
var response = self.env.pos.decodeMercuryResponse(data);
response.payment_method_id = parsed_result.payment_method_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.env.pos.get_order();
if (swipe_pending_line) {
order.select_paymentline(swipe_pending_line);
} else {
order.add_paymentline(
self.payment_methods_by_id[parsed_result.payment_method_id]
);
}
order.selected_paymentline.paid = true;
order.selected_paymentline.mercury_swipe_pending = false;
order.selected_paymentline.mercury_amount = response.authorize;
order.selected_paymentline.set_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();
NumberBuffer.reset();
order.trigger('change', order); // needed so that export_to_JSON gets triggered
self.render();
if (response.message === 'PARTIAL AP') {
def.resolve({
message: self.env._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,
});
}
}
})
.catch(function () {
self.retry_mercury_transaction(
def,
null,
retry_nr,
false,
self.credit_code_transaction,
[parsed_result, def, retry_nr + 1]
);
});
}
credit_code_cancel() {
return;
}
credit_code_action(parsed_result) {
var online_payment_methods = this.env.pos.getOnlinePaymentMethods();
if (online_payment_methods.length === 1) {
parsed_result.payment_method_id = online_payment_methods[0].item;
this.credit_code_transaction(parsed_result);
} else {
// this is for supporting another payment system like mercury
const selectionList = online_payment_methods.map((paymentMethod) => ({
id: paymentMethod.item,
label: paymentMethod.label,
isSelected: false,
item: paymentMethod.item,
}));
this.showPopup('SelectionPopup', {
title: this.env._t('Pay with: '),
list: selectionList,
}).then(({ confirmed, payload: selectedPaymentMethod }) => {
if (confirmed) {
parsed_result.payment_method_id = selectedPaymentMethod;
this.credit_code_transaction(parsed_result);
} else {
this.credit_code_cancel();
}
});
}
}
remove_paymentline_by_ref(line) {
this.env.pos.get_order().remove_paymentline(line);
NumberBuffer.reset();
this.render();
}
do_reversal(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.showPopup('PaymentTransactionPopup', {
transaction: def,
});
var request_data = _.extend(
{
transaction_type: 'Credit',
transaction_code: 'VoidSaleByRecordNo',
},
line.mercury_data
);
var message = '';
var rpc_method = '';
if (is_voidsale) {
message = this.env._t('Reversal failed, sending VoidSale...');
rpc_method = 'do_voidsale';
} else {
message = this.env._t('Sending reversal...');
rpc_method = 'do_reversal';
}
if (!old_deferred) {
def.notify({
message: message,
});
}
this.rpc(
{
model: 'pos_mercury.mercury_transaction',
method: rpc_method,
args: [request_data],
},
{
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: self.env._t('Odoo error while processing transaction.'),
});
return;
}
var response = self.env.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: self.env._t('Reversal succeeded'),
});
self.remove_paymentline_by_ref(line);
}
} else {
// voidsale ended, nothing more we can do
if (response.status === 'Approved') {
def.resolve({
message: self.env._t('VoidSale succeeded'),
});
self.remove_paymentline_by_ref(line);
} else {
def.resolve({
message:
'Error ' + response.error + ':<br/>' + response.message,
});
}
}
})
.catch(function () {
self.retry_mercury_transaction(
def,
null,
retry_nr,
false,
self.do_reversal,
[line, is_voidsale, def, retry_nr + 1]
);
});
}
/**
* @override
*/
deletePaymentLine(event) {
const { cid } = event.detail;
const line = this.paymentLines.find((line) => line.cid === cid);
if (line.mercury_data) {
this.do_reversal(line, false);
} else {
super.deletePaymentLine(event);
}
}
/**
* @override
*/
addNewPaymentLine({ detail: paymentMethod }) {
const order = this.env.pos.get_order();
const res = super.addNewPaymentLine(...arguments);
if (res && paymentMethod.pos_mercury_config_id) {
order.selected_paymentline.mercury_swipe_pending = true;
order.trigger('change', order);
this.render();
}
}
};
Registries.Component.extend(PaymentScreen, PosMercuryPaymentScreen);
return PaymentScreen;
});
@@ -0,0 +1,30 @@
odoo.define('pos_mercury.PaymentScreenPaymentLines', function (require) {
'use strict';
const PaymentScreenPaymentLines = require('point_of_sale.PaymentScreenPaymentLines');
const Registries = require('point_of_sale.Registries');
const PosMercuryPaymentLines = (PaymentScreenPaymentLines) =>
class extends PaymentScreenPaymentLines {
/**
* @override
*/
selectedLineClass(line) {
return Object.assign({}, super.selectedLineClass(line), {
o_pos_mercury_swipe_pending: line.mercury_swipe_pending,
});
}
/**
* @override
*/
unselectedLineClass(line) {
return Object.assign({}, super.unselectedLineClass(line), {
o_pos_mercury_swipe_pending: line.mercury_swipe_pending,
});
}
};
Registries.Component.extend(PaymentScreenPaymentLines, PosMercuryPaymentLines);
return PaymentScreenPaymentLines;
});
@@ -0,0 +1,37 @@
odoo.define('pos_mercury.PaymentTransactionPopup', function(require) {
'use strict';
const { useState } = owl.hooks;
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
class PaymentTransactionPopup extends AbstractAwaitablePopup {
constructor() {
super(...arguments);
this.state = useState({ message: '', confirmButtonIsShown: false });
this.props.transaction.then(data => {
if (data.auto_close) {
setTimeout(() => {
this.confirm();
}, 2000)
} else {
this.state.confirmButtonIsShown = true;
}
this.state.message = data.message;
}).progress(data => {
this.state.message = data.message;
})
}
}
PaymentTransactionPopup.template = 'PaymentTransactionPopup';
PaymentTransactionPopup.defaultProps = {
confirmText: 'Ok',
cancelText: 'Cancel',
title: 'Online Payment',
body: '',
};
Registries.Component.add(PaymentTransactionPopup);
return PaymentTransactionPopup;
});
@@ -0,0 +1,26 @@
odoo.define('pos_mercury.ProductScreen', function (require) {
'use strict';
const ProductScreen = require('point_of_sale.ProductScreen');
const Registries = require('point_of_sale.Registries');
const { useBarcodeReader } = require('point_of_sale.custom_hooks');
const PosMercuryProductScreen = (ProductScreen) =>
class extends ProductScreen {
constructor() {
super(...arguments);
useBarcodeReader({
credit: this.credit_error_action,
});
}
credit_error_action() {
this.showPopup('ErrorPopup', {
body: this.env._t('Go to payment screen to use cards'),
});
}
};
Registries.Component.extend(ProductScreen, PosMercuryProductScreen);
return ProductScreen;
});
@@ -1,18 +1,8 @@
odoo.define('pos_mercury.pos_mercury', function (require) {
"use strict";
var core = require('web.core');
var rpc = require('web.rpc');
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 ScreenWidget = screens.ScreenWidget;
var PaymentScreenWidget = screens.PaymentScreenWidget;
pos_model.load_fields('pos.payment.method', 'pos_mercury_config_id');
pos_model.PosModel = pos_model.PosModel.extend({
@@ -140,508 +130,4 @@ pos_model.Order = pos_model.Order.extend({
},
});
// Lookup table to store status and error messages
var lookUpCodeTransaction = {
'Approved': {
'000000': _t('Transaction approved'),
},
'TimeoutError': {
'001006': 'Global API Not Initialized',
'001007': 'Timeout on Response',
'003003': 'Socket Error sending request',
'003004': 'Socket already open or in use',
'003005': 'Socket Creation Failed',
'003006': 'Socket Connection Failed',
'003007': 'Connection Lost',
'003008': 'TCP/IP Failed to Initialize',
'003010': 'Time Out waiting for server response',
'003011': 'Connect Canceled',
'003053': 'Initialize Failed',
'009999': 'Unknown Error',
},
'FatalError': {
'-1': 'Timeout error',
'001001': 'General Failure',
'001003': 'Invalid Command Format',
'001004': 'Insufficient Fields',
'001011': 'Empty Command String',
'002000': 'Password Verified',
'002001': 'Queue Full',
'002002': 'Password Failed – Disconnecting',
'002003': 'System Going Offline',
'002004': 'Disconnecting Socket',
'002006': 'Refused ‘Max Connections’',
'002008': 'Duplicate Serial Number Detected',
'002009': 'Password Failed (Client / Server)',
'002010': 'Password failed (Challenge / Response)',
'002011': 'Internal Server Error – Call Provider',
'003002': 'In Process with server',
'003009': 'Control failed to find branded serial (password lookup failed)',
'003012': '128 bit CryptoAPI failed',
'003014': 'Threaded Auth Started Expect Response',
'003017': 'Failed to start Event Thread.',
'003050': 'XML Parse Error',
'003051': 'All Connections Failed',
'003052': 'Server Login Failed',
'004001': 'Global Response Length Error (Too Short)',
'004002': 'Unable to Parse Response from Global (Indistinguishable)',
'004003': 'Global String Error',
'004004': 'Weak Encryption Request Not Supported',
'004005': 'Clear Text Request Not Supported',
'004010': 'Unrecognized Request Format',
'004011': 'Error Occurred While Decrypting Request',
'004017': 'Invalid Check Digit',
'004018': 'Merchant ID Missing',
'004019': 'TStream Type Missing',
'004020': 'Could Not Encrypt Response- Call Provider',
'100201': 'Invalid Transaction Type',
'100202': 'Invalid Operator ID',
'100203': 'Invalid Memo',
'100204': 'Invalid Account Number',
'100205': 'Invalid Expiration Date',
'100206': 'Invalid Authorization Code',
'100207': 'Invalid Authorization Code',
'100208': 'Invalid Authorization Amount',
'100209': 'Invalid Cash Back Amount',
'100210': 'Invalid Gratuity Amount',
'100211': 'Invalid Purchase Amount',
'100212': 'Invalid Magnetic Stripe Data',
'100213': 'Invalid PIN Block Data',
'100214': 'Invalid Derived Key Data',
'100215': 'Invalid State Code',
'100216': 'Invalid Date of Birth',
'100217': 'Invalid Check Type',
'100218': 'Invalid Routing Number',
'100219': 'Invalid TranCode',
'100220': 'Invalid Merchant ID',
'100221': 'Invalid TStream Type',
'100222': 'Invalid Batch Number',
'100223': 'Invalid Batch Item Count',
'100224': 'Invalid MICR Input Type',
'100225': 'Invalid Driver’s License',
'100226': 'Invalid Sequence Number',
'100227': 'Invalid Pass Data',
'100228': '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.getOnlinePaymentMethods().length !== 0) {
this.pos.barcode_reader.set_action_callback('credit', _.bind(this.credit_error_action, this));
}
}
});
// On Payment screen, allow online payments
PaymentScreenWidget.include({
// How long we wait for the odoo server to deliver the response of
// a Vantiv transaction
server_timeout_in_ms: 95000,
// How many Vantiv 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 Vantiv (Vantiv 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) {
var order = this.pos.get_order();
if (order.get_due(order.selected_paymentline) < 0) {
this.gui.show_popup('error',{
'title': _t('Refunds not supported'),
'body': _t('Credit card refunds are not supported. Instead select your credit card payment method, click \'Validate\' and refund the original charge manually through the Vantiv backend.'),
});
return;
}
if(this.pos.getOnlinePaymentMethods().length === 0) {
return;
}
var self = this;
var decodedMagtek = self.pos.decodeMagtek(parsed_result.code);
if (! decodedMagtek) {
this.gui.show_popup('error',{
'title': _t('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,
'payment_method_id' : parsed_result.payment_method_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...'),
});
}
rpc.query({
model: 'pos_mercury.mercury_transaction',
method: 'do_payment',
args: [transaction],
}, {
timeout: self.server_timeout_in_ms,
})
.then(function (data) {
// if not receiving a response from Vantiv, 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 Vantiv merchant account.")
});
return;
}
if (data === "internal error") {
def.resolve({
message: _t("Odoo error while processing transaction.")
});
return;
}
var response = self.pos.decodeMercuryResponse(data);
response.payment_method_id = parsed_result.payment_method_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.payment_methods_by_id[parsed_result.payment_method_id]);
}
order.selected_paymentline.paid = true;
order.selected_paymentline.mercury_swipe_pending = false;
order.selected_paymentline.mercury_amount = response.authorize;
order.selected_paymentline.set_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
});
}
}
}).catch(function () {
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) {
var self = this;
var online_payment_methods = this.pos.getOnlinePaymentMethods();
if (online_payment_methods.length === 1) {
parsed_result.payment_method_id = online_payment_methods[0].item;
self.credit_code_transaction(parsed_result);
} else { // this is for supporting another payment system like mercury
this.gui.show_popup('selection',{
title: _t('Pay with: '),
list: online_payment_methods,
confirm: function (item) {
parsed_result.payment_method_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,
});
}
rpc.query({
model: 'pos_mercury.mercury_transaction',
method: rpc_method,
args: [request_data],
}, {
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,
});
}
}
}).catch(function () {
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 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 order = this.pos.get_order();
var payment_method = this.pos.payment_methods_by_id[id];
var res = this._super(id);
if (res && payment_method.pos_mercury_config_id) {
order.selected_paymentline.mercury_swipe_pending = true;
this.render_paymentlines();
order.trigger('change', order); // needed so that export_to_JSON gets triggered
}
},
show: function () {
this._super();
if (this.pos.getOnlinePaymentMethods().length !== 0) {
this.pos.barcode_reader.set_action_callback('credit', _.bind(this.credit_code_action, this));
}
},
});
});
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_mercury.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
<xpath expr="//t[@t-foreach='paymentlines']" position="inside">
<t t-if="line.mercury_data">
<div class="pos-receipt-left-padding">
<span>APPROVAL CODE: </span>
<span>
<t t-esc="line.mercury_auth_code" />
</span>
</div>
</t>
</xpath>
<xpath expr="//div[hasclass('pos-receipt-order-data')]" position="after">
<div t-if="hasPosMercurySignature">
<br />
<div>CARDHOLDER WILL PAY CARD ISSUER</div>
<div>ABOVE AMOUNT PURSUANT</div>
<div>TO CARDHOLDER AGREEMENT</div>
<br />
<br />
<div>X______________________________</div>
</div>
</xpath>
</t>
</templates>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_mercury.PaymentScreenPaymentLines" t-inherit="point_of_sale.PaymentScreenPaymentLines" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('col-name')]//t[@t-esc='line.payment_method.name']" position="replace">
<t t-if="!line.payment_method.is_cash_count and line.mercury_swipe_pending">
<span>WAITING FOR SWIPE</span>
</t>
<t t-else="">
<t t-esc="line.payment_method.name" />
</t>
</xpath>
</t>
</templates>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="PaymentTransactionPopup" owl="1">
<div role="dialog" class="modal-dialog">
<div class="popup">
<p class="title">
<t t-esc="props.title"></t>
</p>
<p class="body">
<t t-esc="state.message"></t>
</p>
<div t-if="state.confirmButtonIsShown" class="footer">
<div class="button cancel" t-on-click="confirm">
<t t-esc="props.confirmText"></t>
</div>
</div>
</div>
</div>
</t>
</templates>
@@ -1,72 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" inherit_id="point_of_sale.template">
<t t-name="PaymentTransactionPopupWidget">
<div role="dialog" class="modal-dialog">
<div class="popup">
<p class="title">Online 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.payment_method.is_cash_count">
<t t-if="line.mercury_swipe_pending">
<div>WAITING FOR SWIPE</div>
</t>
<t t-if="! line.mercury_swipe_pending">
<t t-esc='line.payment_method.name' />
</t>
</t>
<t t-if="line.payment_method.is_cash_count">
<t t-esc='line.payment_method.name' />
</t>
</t>
<!-- if there is payment status, then it is terminal paymentline, so payment-terminal class should be added. -->
<t t-jquery=".body div[t-attf-class*='paymentline selected']">
this.removeAttr('class');
this.attr('t-attf-class', 'paymentline selected#{(line.get_payment_status() ? \' payment-terminal\' : \'\') + (line.mercury_swipe_pending ? \' o_pos_mercury_swipe_pending\' : \'\')}');
</t>
<t t-jquery=".body .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=".body .col-tendered.edit">
this.removeAttr('class');
this.attr('t-attf-class', 'col-tendered edit #{line.mercury_swipe_pending ? \'o_pos_mercury_swipe_pending\' : \'\'}');
</t>
</t>
<t t-name="PosMercurySignature">
<t t-foreach="paymentlines" t-as="paymentline">
<t t-if="paymentline.mercury_data &amp;&amp; ! printed_signature">
<br />
<div>CARDHOLDER WILL PAY CARD ISSUER</div>
<div>ABOVE AMOUNT PURSUANT</div>
<div>TO CARDHOLDER AGREEMENT</div>
<br />
<br />
<div>X______________________________</div>
<t t-set="printed_signature" t-value="true"/>
</t>
</t>
</t>
<t t-extend="OrderReceipt">
<t t-jquery="t[t-foreach*='paymentlines'][t-as*='line']" t-operation="append">
<t t-if="line.mercury_data">
<div class="pos-receipt-left-padding">
APPROVAL CODE: <t t-esc="line.mercury_auth_code"/>
</div>
</t>
</t>
<t t-jquery="receipt" t-operation="append">
<div>
<t t-call="PosMercurySignature"/>
</div>
</t>
</t>
</templates>
@@ -5,6 +5,11 @@
<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>
<script type="text/javascript" src="/pos_mercury/static/src/js/OrderReceipt.js"></script>
<script type="text/javascript" src="/pos_mercury/static/src/js/PaymentScreen.js"></script>
<script type="text/javascript" src="/pos_mercury/static/src/js/PaymentScreenPaymentLines.js"></script>
<script type="text/javascript" src="/pos_mercury/static/src/js/PaymentTransactionPopup.js"></script>
<script type="text/javascript" src="/pos_mercury/static/src/js/ProductScreen.js"></script>
<link rel="stylesheet" href="/pos_mercury/static/src/css/pos_mercury.css"/>
</xpath>
</template>