mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[MOV] pos_elavon: from hibou-suite-enterprise:11.0
This commit is contained in:
1
pos_elavon/__init__.py
Normal file
1
pos_elavon/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
41
pos_elavon/__manifest__.py
Normal file
41
pos_elavon/__manifest__.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
'name': 'Elavon Payment Services',
|
||||||
|
'version': '11.0.1.0.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 Elavon.
|
||||||
|
An Elavon 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
|
||||||
|
""",
|
||||||
|
'depends': [
|
||||||
|
'web',
|
||||||
|
'barcodes',
|
||||||
|
'pos_sale',
|
||||||
|
],
|
||||||
|
'website': 'https://hibou.io',
|
||||||
|
'data': [
|
||||||
|
'data/pos_elavon_data.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/pos_elavon_templates.xml',
|
||||||
|
'views/pos_elavon_views.xml',
|
||||||
|
'views/pos_config_setting_views.xml',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'data/pos_elavon_demo.xml',
|
||||||
|
],
|
||||||
|
'qweb': [
|
||||||
|
'static/src/xml/pos_elavon.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
13
pos_elavon/data/pos_elavon_data.xml
Normal file
13
pos_elavon/data/pos_elavon_data.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<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>
|
||||||
|
</odoo>
|
||||||
12
pos_elavon/data/pos_elavon_demo.xml
Normal file
12
pos_elavon/data/pos_elavon_demo.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Elavon Test Account -->
|
||||||
|
<!-- This is a test account for testing with test cards and cannot be used in a live environment -->
|
||||||
|
<record id="pos_elavon_configuration" model="pos_elavon.configuration">
|
||||||
|
<field name="name">Elavon Demo</field>
|
||||||
|
<field name="merchant_id">123</field>
|
||||||
|
<field name="merchant_user_id">POS</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
2
pos_elavon/models/__init__.py
Normal file
2
pos_elavon/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import pos_elavon
|
||||||
|
from . import pos_elavon_transaction
|
||||||
82
pos_elavon/models/pos_elavon.py
Normal file
82
pos_elavon/models/pos_elavon.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.tools.float_utils import float_compare
|
||||||
|
|
||||||
|
|
||||||
|
class BarcodeRule(models.Model):
|
||||||
|
_inherit = 'barcode.rule'
|
||||||
|
|
||||||
|
type = fields.Selection(selection_add=[
|
||||||
|
('credit', 'Credit Card')
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class CRMTeam(models.Model):
|
||||||
|
_inherit = 'crm.team'
|
||||||
|
|
||||||
|
pos_elavon_merchant_pin = fields.Char(string='POS Elavon Merchant PIN')
|
||||||
|
|
||||||
|
|
||||||
|
class PosElavonConfiguration(models.Model):
|
||||||
|
_name = 'pos_elavon.configuration'
|
||||||
|
|
||||||
|
name = fields.Char(required=True, help='Name of this Elavon configuration')
|
||||||
|
merchant_id = fields.Char(string='Merchant ID', required=True, help='ID of the merchant to authenticate him on the payment provider server')
|
||||||
|
merchant_user_id = fields.Char(string='Merchant User ID', required=True, help='User ID, e.g. POS')
|
||||||
|
|
||||||
|
|
||||||
|
class AccountBankStatementLine(models.Model):
|
||||||
|
_inherit = "account.bank.statement.line"
|
||||||
|
|
||||||
|
elavon_card_number = fields.Char(string='Card Number', help='Masked credit card.')
|
||||||
|
elavon_txn_id = fields.Char(string='Elavon Transaction ID')
|
||||||
|
|
||||||
|
|
||||||
|
class AccountJournal(models.Model):
|
||||||
|
_inherit = 'account.journal'
|
||||||
|
|
||||||
|
pos_elavon_config_id = fields.Many2one('pos_elavon.configuration', string='Elavon Credentials',
|
||||||
|
help='The configuration of Elavon that can be used with this journal.')
|
||||||
|
|
||||||
|
|
||||||
|
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({
|
||||||
|
'elavon_card_number': ui_paymentline.get('elavon_card_number'),
|
||||||
|
'elavon_txn_id': ui_paymentline.get('elavon_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.elavon_card_number:
|
||||||
|
line.elavon_card_number = data.get('elavon_card_number')
|
||||||
|
line.elavon_txn_id = data.get('elavon_txn_id')
|
||||||
|
break
|
||||||
|
|
||||||
|
return statement_id
|
||||||
|
|
||||||
|
|
||||||
|
class AutoVacuum(models.AbstractModel):
|
||||||
|
_inherit = 'ir.autovacuum'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def power_on(self, *args, **kwargs):
|
||||||
|
self.env['pos_elavon.elavon_transaction'].cleanup_old_tokens()
|
||||||
|
return super(AutoVacuum, self).power_on(*args, **kwargs)
|
||||||
128
pos_elavon/models/pos_elavon_transaction.py
Normal file
128
pos_elavon/models/pos_elavon_transaction.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import werkzeug
|
||||||
|
|
||||||
|
from odoo import models, api, service
|
||||||
|
from odoo.tools.translate import _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, misc
|
||||||
|
|
||||||
|
|
||||||
|
class ElavonTransaction(models.Model):
|
||||||
|
_name = 'pos_elavon.elavon_transaction'
|
||||||
|
|
||||||
|
def _get_pos_session(self):
|
||||||
|
pos_session = self.env['pos.session'].search([('state', '=', 'opened'), ('user_id', '=', self.env.uid)], limit=1)
|
||||||
|
if not pos_session:
|
||||||
|
raise UserError(_("No opened point of sale session for user %s found") % self.env.user.name)
|
||||||
|
|
||||||
|
pos_session.login()
|
||||||
|
|
||||||
|
return pos_session
|
||||||
|
|
||||||
|
def _get_pos_elavon_config_id(self, config, journal_id):
|
||||||
|
journal = config.journal_ids.filtered(lambda r: r.id == journal_id)
|
||||||
|
|
||||||
|
if journal and journal.pos_elavon_config_id:
|
||||||
|
return journal.pos_elavon_config_id
|
||||||
|
else:
|
||||||
|
raise UserError(_("No Elavon configuration associated with the journal."))
|
||||||
|
|
||||||
|
def _setup_request(self, data):
|
||||||
|
# todo: in master make the client include the pos.session id and use that
|
||||||
|
pos_session = self._get_pos_session()
|
||||||
|
|
||||||
|
config = pos_session.config_id
|
||||||
|
|
||||||
|
pos_elavon_config = self._get_pos_elavon_config_id(config, data['journal_id'])
|
||||||
|
|
||||||
|
data['ssl_merchant_id'] = pos_elavon_config.sudo().merchant_id
|
||||||
|
data['ssl_user_id'] = pos_elavon_config.sudo().merchant_user_id
|
||||||
|
# Load from team
|
||||||
|
data['ssl_pin'] = config.sudo().crm_team_id.pos_elavon_merchant_pin
|
||||||
|
data['ssl_show_form'] = 'false'
|
||||||
|
data['ssl_result_format'] = 'ascii'
|
||||||
|
|
||||||
|
|
||||||
|
def _do_request(self, data):
|
||||||
|
if not data['ssl_merchant_id'] or not data['ssl_user_id'] or not data['ssl_pin']:
|
||||||
|
return "not setup"
|
||||||
|
response = ''
|
||||||
|
|
||||||
|
url = 'https://api.convergepay.com/VirtualMerchant/process.do'
|
||||||
|
if self.env['ir.config_parameter'].sudo().get_param('pos_elavon.enable_test_env'):
|
||||||
|
url = 'https://api.demo.convergepay.com/VirtualMerchantDemo/process.do'
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post(url, data=data, timeout=500)
|
||||||
|
r.raise_for_status()
|
||||||
|
response = werkzeug.utils.unescape(r.content.decode())
|
||||||
|
except Exception:
|
||||||
|
response = "timeout"
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _do_reversal_or_voidsale(self, data, is_voidsale):
|
||||||
|
try:
|
||||||
|
self._setup_request(data)
|
||||||
|
except UserError:
|
||||||
|
return "internal error"
|
||||||
|
|
||||||
|
# Can we voidsale?
|
||||||
|
#data['is_voidsale'] = is_voidsale
|
||||||
|
data['ssl_transaction_type'] = 'CCVOID'
|
||||||
|
response = self._do_request(data)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def do_payment(self, data):
|
||||||
|
try:
|
||||||
|
self._setup_request(data)
|
||||||
|
except UserError:
|
||||||
|
return "internal error"
|
||||||
|
|
||||||
|
data['ssl_transaction_type'] = 'CCSALE'
|
||||||
|
response = self._do_request(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)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def do_return(self, data):
|
||||||
|
try:
|
||||||
|
self._setup_request(data)
|
||||||
|
except UserError:
|
||||||
|
return "internal error"
|
||||||
|
|
||||||
|
data['ssl_transaction_type'] = 'CCRETURN'
|
||||||
|
response = self._do_request(data)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def do_credit(self, data):
|
||||||
|
try:
|
||||||
|
self._setup_request(data)
|
||||||
|
except UserError:
|
||||||
|
return "internal error"
|
||||||
|
|
||||||
|
data['ssl_transaction_type'] = 'CCCREDIT'
|
||||||
|
response = self._do_request(data)
|
||||||
|
return response
|
||||||
|
|
||||||
|
# One time (the ones we use) Elavon tokens are required to be
|
||||||
|
# deleted after 6 months
|
||||||
|
# This is a from Mercury, probably not needed anymore.
|
||||||
|
@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 = ""
|
||||||
3
pos_elavon/security/ir.model.access.csv
Normal file
3
pos_elavon/security/ir.model.access.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_pos_elavon_configuration,elavon.configuration,model_pos_elavon_configuration,point_of_sale.group_pos_manager,1,1,1,1
|
||||||
|
access_pos_elavon_elavon_transaction,elavon.transaction,model_pos_elavon_elavon_transaction,,1,0,0,0
|
||||||
|
25
pos_elavon/static/src/css/pos_elavon.css
Normal file
25
pos_elavon/static/src/css/pos_elavon.css
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.pos .paymentline.selected.o_pos_elavon_swipe_pending, .pos .paymentline.o_pos_elavon_swipe_pending {
|
||||||
|
background: rgb(239, 153, 65);
|
||||||
|
}
|
||||||
|
.pos .col-tendered.edit.o_pos_elavon_swipe_pending {
|
||||||
|
color: rgb(239, 153, 65);
|
||||||
|
box-shadow: 0px 0px 0px 3px rgb(239, 153, 65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .paymentline .elavon_manual_transaction {
|
||||||
|
cursor: pointer;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .payment-manual-transaction label {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.pos .payment-manual-transaction label span {
|
||||||
|
font-size: 75%;
|
||||||
|
}
|
||||||
693
pos_elavon/static/src/js/pos_elavon.js
Normal file
693
pos_elavon/static/src/js/pos_elavon.js
Normal file
@@ -0,0 +1,693 @@
|
|||||||
|
odoo.define('pos_elavon.pos_elavon', 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 BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;
|
||||||
|
var PopupWidget = require('point_of_sale.popups');
|
||||||
|
var ScreenWidget = screens.ScreenWidget;
|
||||||
|
var PaymentScreenWidget = screens.PaymentScreenWidget;
|
||||||
|
|
||||||
|
pos_model.load_fields("account.journal", "pos_elavon_config_id");
|
||||||
|
|
||||||
|
pos_model.PosModel = pos_model.PosModel.extend({
|
||||||
|
|
||||||
|
getOnlinePaymentJournals: function () {
|
||||||
|
var self = this;
|
||||||
|
var online_payment_journals = [];
|
||||||
|
|
||||||
|
$.each(this.journals, function (i, val) {
|
||||||
|
if (val.pos_elavon_config_id) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
$.each(this.cashregisters, function (index, cashregister) {
|
||||||
|
if (cashregister.journal_id[0] === journal_id) {
|
||||||
|
cashregister_return = cashregister;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cashregister_return;
|
||||||
|
},
|
||||||
|
|
||||||
|
decodeMagtek: function (s) {
|
||||||
|
if (s.indexOf('%B') < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var to_return = {};
|
||||||
|
to_return['ssl_card_number'] = s.substr(s.indexOf('%B') + 2, s.indexOf('^') - 2);
|
||||||
|
var temp = s.substr(s.indexOf('^') + 1, s.length);
|
||||||
|
name = temp.substr(0, temp.indexOf('/'));
|
||||||
|
var exp = temp.substr(temp.indexOf('^') + 1, 4);
|
||||||
|
to_return['ssl_exp_date'] = exp.substr(2, 2) + exp.substr(0, 2)
|
||||||
|
to_return['ssl_card_present'] = 'Y'
|
||||||
|
to_return['ssl_track_data'] = s
|
||||||
|
return to_return
|
||||||
|
},
|
||||||
|
|
||||||
|
decodeElavonResponse: function (data) {
|
||||||
|
var lines = data.split(/\r?\n/);
|
||||||
|
var to_return = {}
|
||||||
|
|
||||||
|
lines.forEach(function (line){
|
||||||
|
var eq = line.indexOf('=');
|
||||||
|
if (eq > 0) {
|
||||||
|
var name = line.substr(0, eq);
|
||||||
|
var value = line.substr(eq+1);
|
||||||
|
to_return[name] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return to_return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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.elavon_swipe_pending = json.elavon_swipe_pending;
|
||||||
|
this.elavon_card_number = json.elavon_card_number;
|
||||||
|
this.elavon_card_brand = json.elavon_card_brand;
|
||||||
|
this.elavon_txn_id = json.elavon_txn_id;
|
||||||
|
|
||||||
|
this.set_credit_card_name();
|
||||||
|
},
|
||||||
|
export_as_JSON: function () {
|
||||||
|
return _.extend(_paylineproto.export_as_JSON.apply(this, arguments), {
|
||||||
|
paid: this.paid,
|
||||||
|
elavon_swipe_pending: this.elavon_swipe_pending,
|
||||||
|
elavon_card_number: this.elavon_card_number,
|
||||||
|
elavon_card_brand: this.elavon_card_brand,
|
||||||
|
elavon_txn_id: this.elavon_txn_id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
set_credit_card_name: function () {
|
||||||
|
if (this.elavon_card_number) {
|
||||||
|
this.name = this.elavon_card_brand + ' ' + this.elavon_card_number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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});
|
||||||
|
|
||||||
|
// Popup to record manual CC entry
|
||||||
|
var PaymentManualTransactionPopupWidget = PopupWidget.extend({
|
||||||
|
template: 'PaymentManualTransactionPopupWidget',
|
||||||
|
show: function (options) {
|
||||||
|
this._super(options);
|
||||||
|
this.setup_transaction_callback();
|
||||||
|
},
|
||||||
|
setup_transaction_callback: function(){
|
||||||
|
var self = this;
|
||||||
|
this.options.transaction.then(function (data) {
|
||||||
|
if (data.auto_close) {
|
||||||
|
setTimeout(function () {
|
||||||
|
self.gui.close_popup();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
self.$el.find('p.body').html(data.message);
|
||||||
|
}).progress(function (data) {
|
||||||
|
self.$el.find('p.body').html(data.message);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
click_confirm: function(){
|
||||||
|
var values = {
|
||||||
|
ssl_card_number: this.$('input[name="card_number"]').val(),
|
||||||
|
ssl_exp_date: this.$('input[name="exp_date"]').val(),
|
||||||
|
ssl_cvv2cvc2: this.$('input[name="cvv2cvc2"]').val(),
|
||||||
|
};
|
||||||
|
if ( this.options.transaction.state() != 'pending' ) {
|
||||||
|
this.options.transaction = $.Deferred();
|
||||||
|
this.setup_transaction_callback();
|
||||||
|
}
|
||||||
|
if( this.options.confirm ){
|
||||||
|
this.options.confirm.call(this, values, this.options.transaction);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
gui.define_popup({name:'payment-manual-transaction', widget: PaymentManualTransactionPopupWidget});
|
||||||
|
|
||||||
|
// 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.getOnlinePaymentJournals().length !== 0) {
|
||||||
|
this.pos.barcode_reader.set_action_callback('credit', _.bind(this.credit_error_action, this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// On Payment screen, allow electronic payments
|
||||||
|
PaymentScreenWidget.include({
|
||||||
|
// Override init because it eats all keyboard input and we need it for popups...
|
||||||
|
init: function(parent, options) {
|
||||||
|
var self = this;
|
||||||
|
this._super(parent, options);
|
||||||
|
|
||||||
|
// This is a keydown handler that prevents backspace from
|
||||||
|
// doing a back navigation. It also makes sure that keys that
|
||||||
|
// do not generate a keypress in Chrom{e,ium} (eg. delete,
|
||||||
|
// backspace, ...) get passed to the keypress handler.
|
||||||
|
this.keyboard_keydown_handler = function(event){
|
||||||
|
if (event.keyCode === 8 || event.keyCode === 46) { // Backspace and Delete
|
||||||
|
// don't prevent delete if a popup is up
|
||||||
|
if (self.gui.has_popup()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// These do not generate keypress events in
|
||||||
|
// Chrom{e,ium}. Even if they did, we just called
|
||||||
|
// preventDefault which will cancel any keypress that
|
||||||
|
// would normally follow. So we call keyboard_handler
|
||||||
|
// explicitly with this keydown event.
|
||||||
|
self.keyboard_handler(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// This keyboard handler listens for keypress events. It is
|
||||||
|
// also called explicitly to handle some keydown events that
|
||||||
|
// do not generate keypress events.
|
||||||
|
this.keyboard_handler = function(event){
|
||||||
|
// On mobile Chrome BarcodeEvents relies on an invisible
|
||||||
|
// input being filled by a barcode device. Let events go
|
||||||
|
// through when this input is focused.
|
||||||
|
if (self.gui.has_popup()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (BarcodeEvents.$barcodeInput && BarcodeEvents.$barcodeInput.is(":focus")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = '';
|
||||||
|
|
||||||
|
if (event.type === "keypress") {
|
||||||
|
if (event.keyCode === 13) { // Enter
|
||||||
|
self.validate_order();
|
||||||
|
} else if ( event.keyCode === 190 || // Dot
|
||||||
|
event.keyCode === 110 || // Decimal point (numpad)
|
||||||
|
event.keyCode === 188 || // Comma
|
||||||
|
event.keyCode === 46 ) { // Numpad dot
|
||||||
|
key = self.decimal_point;
|
||||||
|
} else if (event.keyCode >= 48 && event.keyCode <= 57) { // Numbers
|
||||||
|
key = '' + (event.keyCode - 48);
|
||||||
|
} else if (event.keyCode === 45) { // Minus
|
||||||
|
key = '-';
|
||||||
|
} else if (event.keyCode === 43) { // Plus
|
||||||
|
key = '+';
|
||||||
|
}
|
||||||
|
} else { // keyup/keydown
|
||||||
|
if (event.keyCode === 46) { // Delete
|
||||||
|
key = 'CLEAR';
|
||||||
|
} else if (event.keyCode === 8) { // Backspace
|
||||||
|
key = 'BACKSPACE';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.payment_input(key);
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// end init override...
|
||||||
|
|
||||||
|
// How long we wait for the odoo server to deliver the response of
|
||||||
|
// a Elavon transaction
|
||||||
|
server_timeout_in_ms: 120000,
|
||||||
|
|
||||||
|
// How many Elavon 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].elavon_swipe_pending) {
|
||||||
|
return lines[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hunt around for an existing line by amount etc.
|
||||||
|
_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].get_amount() === amount &&
|
||||||
|
lines[i].elavon_card_number === card_number &&
|
||||||
|
lines[i].elavon_card_brand === card_brand) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
retry_elavon_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) {
|
||||||
|
// what?
|
||||||
|
//message = "Error " + response.error + ": " + lookUpCodeTransaction["TimeoutError"][response.error] + "<br/>" + response.message;
|
||||||
|
} else {
|
||||||
|
if (can_connect_to_server) {
|
||||||
|
message = _t("No response from Elavon (Elavon 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(this.pos.getOnlinePaymentJournals().length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var transaction = {};
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle manual or swiped
|
||||||
|
if (parsed_result.ssl_card_number) {
|
||||||
|
transaction = parsed_result;
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
transaction = decodedMagtek;
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint = 'do_payment';
|
||||||
|
if (purchase_amount < 0.0) {
|
||||||
|
purchase_amount = -purchase_amount;
|
||||||
|
endpoint = 'do_credit';
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction['ssl_amount'] = purchase_amount.toFixed(2); // This is the format and type that Elavon is expecting
|
||||||
|
transaction['ssl_invoice_number'] = self.pos.get_order().uid;
|
||||||
|
transaction['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...'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc.query({
|
||||||
|
model: 'pos_elavon.elavon_transaction',
|
||||||
|
method: endpoint,
|
||||||
|
args: [transaction],
|
||||||
|
}, {
|
||||||
|
timeout: self.server_timeout_in_ms,
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
// if not receiving a response from Elavon, we should retry
|
||||||
|
if (data === "timeout") {
|
||||||
|
self.retry_elavon_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 Elavon merchant account.")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data === "internal error") {
|
||||||
|
def.resolve({
|
||||||
|
message: _t("Odoo error while processing transaction.")
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = self.pos.decodeElavonResponse(data);
|
||||||
|
result.journal_id = parsed_result.journal_id;
|
||||||
|
|
||||||
|
// Example error data:
|
||||||
|
// errorCode=4025
|
||||||
|
// errorName=Invalid Credentials
|
||||||
|
// errorMessage=The credentials supplied in the authorization request are invalid.
|
||||||
|
var approval_code = result.ssl_approval_code;
|
||||||
|
if (endpoint == 'do_payment' && (!approval_code || !approval_code.trim())) {
|
||||||
|
def.resolve({
|
||||||
|
message: "Error (" + (result.ssl_result_message || result.errorName) + ")",
|
||||||
|
auto_close: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle ssl_approval_code (only seen empty ones so far)
|
||||||
|
if (false /* duplicate transaction detected */) {
|
||||||
|
def.resolve({
|
||||||
|
message: result.ssl_result_message,
|
||||||
|
auto_close: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// any other edge cases or failures?
|
||||||
|
|
||||||
|
if (result.ssl_result_message == 'APPROVAL' || result.ssl_result_message == 'PARTIAL APPROVAL') {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
var amount = parseFloat(result.ssl_amount);
|
||||||
|
if (endpoint == 'do_credit') {
|
||||||
|
amount = -amount;
|
||||||
|
}
|
||||||
|
order.selected_paymentline.set_amount(amount);
|
||||||
|
order.selected_paymentline.paid = true;
|
||||||
|
order.selected_paymentline.elavon_swipe_pending = false;
|
||||||
|
order.selected_paymentline.elavon_card_number = result.ssl_card_number;
|
||||||
|
order.selected_paymentline.elavon_card_brand = result.ssl_card_short_description;
|
||||||
|
if (result.ssl_card_short_description) {
|
||||||
|
order.selected_paymentline.elavon_card_brand = result.ssl_card_short_description;
|
||||||
|
} else {
|
||||||
|
order.selected_paymentline.elavon_card_brand = result.ssl_card_type;
|
||||||
|
}
|
||||||
|
order.selected_paymentline.elavon_txn_id = result.ssl_txn_id;
|
||||||
|
// maybe approval code....
|
||||||
|
order.selected_paymentline.set_credit_card_name();
|
||||||
|
|
||||||
|
self.order_changes();
|
||||||
|
self.reset_input();
|
||||||
|
self.render_paymentlines();
|
||||||
|
order.trigger('change', order);
|
||||||
|
def.resolve({
|
||||||
|
message: result.ssl_result_message + ' : ' + order.selected_paymentline.elavon_txn_id,
|
||||||
|
auto_close: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}).fail(function (type, error) {
|
||||||
|
self.retry_elavon_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_journals = this.pos.getOnlinePaymentJournals();
|
||||||
|
|
||||||
|
if (online_payment_journals.length === 1) {
|
||||||
|
parsed_result.journal_id = online_payment_journals[0].item;
|
||||||
|
self.credit_code_transaction(parsed_result);
|
||||||
|
} else { // this is for supporting another payment system like elavon
|
||||||
|
this.gui.show_popup('selection',{
|
||||||
|
title: 'Pay ' + this.pos.get_order().get_due().toFixed(2) + ' with : ',
|
||||||
|
list: online_payment_journals,
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO Maybe do this, as it might be convenient to store the data in json and then do updates to it
|
||||||
|
// var request_data = _.extend({
|
||||||
|
// 'transaction_type': 'Credit',
|
||||||
|
// 'transaction_code': 'VoidSaleByRecordNo',
|
||||||
|
// }, line.elavon_data);
|
||||||
|
|
||||||
|
|
||||||
|
// TODO Do we need these options?
|
||||||
|
// 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";
|
||||||
|
// }
|
||||||
|
|
||||||
|
var request_data = {
|
||||||
|
'ssl_txn_id': line.elavon_txn_id,
|
||||||
|
'journal_id': line.cashregister.journal_id[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (! old_deferred) {
|
||||||
|
def.notify({
|
||||||
|
message: 'Sending reversal...',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc.query({
|
||||||
|
model: 'pos_elavon.elavon_transaction',
|
||||||
|
method: 'do_reversal',
|
||||||
|
args: [request_data],
|
||||||
|
}, {
|
||||||
|
timeout: self.server_timeout_in_ms
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
if (data === "timeout") {
|
||||||
|
self.retry_elavon_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 result = self.pos.decodeElavonResponse(data);
|
||||||
|
if (result.ssl_result_message == 'APPROVAL') {
|
||||||
|
def.resolve({
|
||||||
|
message: 'Reversal succeeded.'
|
||||||
|
});
|
||||||
|
self.remove_paymentline_by_ref(line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.errorCode == '5040') {
|
||||||
|
// Already removed.
|
||||||
|
def.resolve({
|
||||||
|
message: 'Invalid Transaction ID. This probably means that it was already reversed.',
|
||||||
|
});
|
||||||
|
self.remove_paymentline_by_ref(line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
def.resolve({
|
||||||
|
message: 'Unknown message check console logs. ' + result.ssl_result_message,
|
||||||
|
});
|
||||||
|
|
||||||
|
}).fail(function (type, error) {
|
||||||
|
self.retry_elavon_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].elavon_txn_id) {
|
||||||
|
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 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_elavon_config_id) {
|
||||||
|
var pending_swipe_line = this._get_swipe_pending_line();
|
||||||
|
|
||||||
|
if (pending_swipe_line) {
|
||||||
|
this.gui.show_popup('error',{
|
||||||
|
'title': _t('Error'),
|
||||||
|
'body': _t('One credit card swipe already pending.'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._super(id);
|
||||||
|
order.selected_paymentline.elavon_swipe_pending = true;
|
||||||
|
this.render_paymentlines();
|
||||||
|
order.trigger('change', order); // needed so that export_to_JSON gets triggered
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._super(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
click_elavon_manual_transaction: function (id) {
|
||||||
|
var self = this;
|
||||||
|
var def = new $.Deferred();
|
||||||
|
var pending_swipe_line = this._get_swipe_pending_line();
|
||||||
|
if (!pending_swipe_line) {
|
||||||
|
this.gui.show_popup('error',{
|
||||||
|
'title': _t('Error'),
|
||||||
|
'body': _t('No swipe pending payment line for manual transaction.'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gui.show_popup('payment-manual-transaction', {
|
||||||
|
transaction: def,
|
||||||
|
confirm: function(card_details, deffered) {
|
||||||
|
card_details.journal_id = pending_swipe_line.cashregister.journal.id;
|
||||||
|
self.credit_code_transaction(card_details, deffered);
|
||||||
|
def.notify({message: _t('Handling transaction...')});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
show: function () {
|
||||||
|
this._super();
|
||||||
|
if (this.pos.getOnlinePaymentJournals().length !== 0) {
|
||||||
|
this.pos.barcode_reader.set_action_callback('credit', _.bind(this.credit_code_action, this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render_paymentlines: function() {
|
||||||
|
this._super();
|
||||||
|
var self = this;
|
||||||
|
self.$('.paymentlines-container').on('click', '.elavon_manual_transaction', function(){
|
||||||
|
self.click_elavon_manual_transaction();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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].elavon_swipe_pending) {
|
||||||
|
this.pos.get_order().remove_paymentline(lines[i]);
|
||||||
|
this.render_paymentlines();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._super(force_validation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
114
pos_elavon/static/src/xml/pos_elavon.xml
Normal file
114
pos_elavon/static/src/xml/pos_elavon.xml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<?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-name="PaymentManualTransactionPopupWidget">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="popup payment-manual-transaction">
|
||||||
|
<p class="title">Manual Electronic Payment</p>
|
||||||
|
<label for="card_number">Card Number</label>
|
||||||
|
<input name="card_number" type="text" t-att-value="widget.options.card_number || ''" placeholder="4355111122223333" autocomplete="off"></input>
|
||||||
|
<label for="exp_date">Expiration Date <span>(4 digits)</span></label>
|
||||||
|
<input name="exp_date" type="text" t-att-value="widget.options.exp_date || ''" placeholder="0223" autocomplete="off"></input>
|
||||||
|
<label for="cvv2cvc2">Card Security Code <span>(3 or 4 digits)</span></label>
|
||||||
|
<input name="cvv2cvc2" type="text" t-att-value="widget.options.cvv2cvc2 || ''" placeholder="003" autocomplete="off"></input>
|
||||||
|
<p class="body"></p>
|
||||||
|
<div class="footer">
|
||||||
|
<div class="button confirm">
|
||||||
|
Ok
|
||||||
|
</div>
|
||||||
|
<div class="button cancel">
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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.elavon_swipe_pending">
|
||||||
|
<div>WAITING FOR SWIPE</div>
|
||||||
|
</t>
|
||||||
|
<t t-if="! line.elavon_swipe_pending">
|
||||||
|
<t t-esc='line.name' />
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="btn btn-small elavon_manual_transaction">Manual</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.elavon_swipe_pending ? \'o_pos_elavon_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.elavon_swipe_pending ? \'o_pos_elavon_swipe_pending\' : \'\'}');
|
||||||
|
</t>
|
||||||
|
<t t-jquery="tbody tr td.col-tendered.edit">
|
||||||
|
this.removeAttr('class');
|
||||||
|
this.attr('t-attf-class', 'col-tendered edit #{line.elavon_swipe_pending ? \'o_pos_elavon_swipe_pending\' : \'\'}');
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="PosElavonSignature">
|
||||||
|
<t t-foreach="paymentlines" t-as="paymentline">
|
||||||
|
<t t-if="!gift && paymentline.elavon_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-if="!gift && line.elavon_data">
|
||||||
|
<line line-ratio="1">
|
||||||
|
<left><pre> APPROVAL CODE:</pre><t t-esc="line.elavon_auth_code"/></left>
|
||||||
|
</line>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
<t t-jquery="receipt" t-operation="append">
|
||||||
|
<div>
|
||||||
|
<t t-call="PosElavonSignature"/>
|
||||||
|
</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 && line.elavon_auth_code">
|
||||||
|
&nbsp;&nbsp;APPROVAL CODE: <t t-esc="line.elavon_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="PosElavonSignature"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
18
pos_elavon/views/pos_config_setting_views.xml
Normal file
18
pos_elavon/views/pos_config_setting_views.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="pos_config_view_form_inherit_pos_elavon" model="ir.ui.view">
|
||||||
|
<field name="name">pos.config.form.inherit.elavon</field>
|
||||||
|
<field name="model">pos.config</field>
|
||||||
|
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<div id="btn_use_pos_mercury" position="after">
|
||||||
|
<!-- TODO Put in a button to install Elavon, this will be hidden unless you have Mercury -->
|
||||||
|
<div class="mt16">
|
||||||
|
<button name="%(pos_elavon.action_configuration_form)d" icon="fa-arrow-right" type="action" string="Elavon Accounts" class="btn-link"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
11
pos_elavon/views/pos_elavon_templates.xml
Normal file
11
pos_elavon/views/pos_elavon_templates.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?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_elavon/static/src/js/pos_elavon.js"></script>
|
||||||
|
<link rel="stylesheet" href="/pos_elavon/static/src/css/pos_elavon.css" />
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
117
pos_elavon/views/pos_elavon_views.xml
Normal file
117
pos_elavon/views/pos_elavon_views.xml
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_pos_elavon_configuration_form" model="ir.ui.view" >
|
||||||
|
<field name="name">Elavon Configurations</field>
|
||||||
|
<field name="model">pos_elavon.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>Elavon Configurations</i> define what Elavon account will be used when
|
||||||
|
processing credit card transactions in the Point Of Sale. Setting up a Elavon
|
||||||
|
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 standard card reader devices. It can be connected
|
||||||
|
directly to the Point Of Sale device or it can be connected to the POSBox.
|
||||||
|
</p><p>
|
||||||
|
Using the Elavon 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><p>
|
||||||
|
Note that you will need to setup a 'PIN' number on POS Teams.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<group col="2">
|
||||||
|
<field name="merchant_id"/>
|
||||||
|
<field name="merchant_user_id"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_pos_elavon_configuration_tree" model="ir.ui.view">
|
||||||
|
<field name="name">Elavon Configurations</field>
|
||||||
|
<field name="model">pos_elavon.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">Elavon Configurations</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">pos_elavon.configuration</field>
|
||||||
|
<field name="view_type">form</field>
|
||||||
|
<field name="view_mode">tree,kanban,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="//group[@name='amount_authorized']" position="after">
|
||||||
|
<group attrs="{'invisible': [('type', '!=', 'bank')]}">
|
||||||
|
<field name="pos_elavon_config_id"/>
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="view_account_bank_journal_form_inherited_pos_elavon" model="ir.ui.view">
|
||||||
|
<field name="name">account.bank.journal.form.inherited.pos.elavon</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_elavon_config_id" attrs="{'invisible': [('journal_user', '=', False)]}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="crm_team_view_form_inherit_pos_sale_elavon" model="ir.ui.view">
|
||||||
|
<field name="name">crm.team.form.pos.elavon</field>
|
||||||
|
<field name="model">crm.team</field>
|
||||||
|
<field name="inherit_id" ref="pos_sale.crm_team_view_form_inherit_pos_sale"></field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='company_id']" position="after">
|
||||||
|
<field name="pos_elavon_merchant_pin"/>
|
||||||
|
</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="elavon_card_number"/>
|
||||||
|
<field name="elavon_txn_id"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem parent="point_of_sale.menu_point_config_product" action="pos_elavon.action_configuration_form" id="menu_pos_pos_elavon_config" groups="base.group_no_one" sequence="35"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user