summaryrefslogtreecommitdiff
path: root/addons/payment/static
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/payment/static
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/payment/static')
-rw-r--r--addons/payment/static/description/icon.pngbin0 -> 8789 bytes
-rw-r--r--addons/payment/static/description/icon.svg1
-rw-r--r--addons/payment/static/img/american_express.pngbin0 -> 7156 bytes
-rw-r--r--addons/payment/static/img/bancontact.pngbin0 -> 7075 bytes
-rw-r--r--addons/payment/static/img/cirrus.pngbin0 -> 6787 bytes
-rw-r--r--addons/payment/static/img/codensa_easy_credit.pngbin0 -> 2947 bytes
-rw-r--r--addons/payment/static/img/diners_club_intl.pngbin0 -> 8907 bytes
-rw-r--r--addons/payment/static/img/discover.pngbin0 -> 5880 bytes
-rw-r--r--addons/payment/static/img/eps.pngbin0 -> 5935 bytes
-rw-r--r--addons/payment/static/img/giropay.pngbin0 -> 10372 bytes
-rw-r--r--addons/payment/static/img/ideal.pngbin0 -> 2262 bytes
-rw-r--r--addons/payment/static/img/jcb.pngbin0 -> 4096 bytes
-rw-r--r--addons/payment/static/img/maestro.pngbin0 -> 8014 bytes
-rw-r--r--addons/payment/static/img/mastercard.pngbin0 -> 8962 bytes
-rw-r--r--addons/payment/static/img/p24.pngbin0 -> 5620 bytes
-rw-r--r--addons/payment/static/img/unionpay.pngbin0 -> 9536 bytes
-rw-r--r--addons/payment/static/img/visa.pngbin0 -> 4900 bytes
-rw-r--r--addons/payment/static/img/webmoney.pngbin0 -> 24974 bytes
-rw-r--r--addons/payment/static/img/western_union.pngbin0 -> 22772 bytes
-rw-r--r--addons/payment/static/lib/jquery.payment/jquery.payment.js652
-rw-r--r--addons/payment/static/src/js/payment_form.js588
-rw-r--r--addons/payment/static/src/js/payment_portal.js65
-rw-r--r--addons/payment/static/src/js/payment_processing.js121
-rw-r--r--addons/payment/static/src/scss/payment_acquirer.scss62
-rw-r--r--addons/payment/static/src/scss/payment_form.scss56
-rw-r--r--addons/payment/static/src/scss/portal_payment.scss61
-rw-r--r--addons/payment/static/src/xml/payment_processing.xml150
27 files changed, 1756 insertions, 0 deletions
diff --git a/addons/payment/static/description/icon.png b/addons/payment/static/description/icon.png
new file mode 100644
index 00000000..bd9a4e06
--- /dev/null
+++ b/addons/payment/static/description/icon.png
Binary files differ
diff --git a/addons/payment/static/description/icon.svg b/addons/payment/static/description/icon.svg
new file mode 100644
index 00000000..cff0a33b
--- /dev/null
+++ b/addons/payment/static/description/icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="98.616%"><stop offset="0%" stop-color="#797C79"/><stop offset="100%" stop-color="#545554"/></linearGradient><path id="d" d="M19.25 34.621C20.983 35.541 22.9 36 25 36c4 0 7-1.667 9-5h15.036v-3.212a.342.342 0 0 0-.339-.343H35c.347-.502.423-1.416.228-2.742h13.808c1.5 0 2.714 1.228 2.714 2.742v20.11c0 1.514-1.213 2.742-2.714 2.742H21.964c-1.5 0-2.714-1.228-2.714-2.742V34.62zm29.447 12.934a.342.342 0 0 0 .339-.343V37.5H21.964v9.712c0 .188.152.343.339.343h26.394zm-18.614-5.713v2.285a.683.683 0 0 1-.677.685h-4.062a.683.683 0 0 1-.677-.685v-2.285c0-.377.304-.686.677-.686h4.062c.373 0 .677.309.677.686zm10.834 0v2.285a.683.683 0 0 1-.677.685h-7.674a.683.683 0 0 1-.677-.685v-2.285c0-.377.305-.686.677-.686h7.674c.372 0 .677.309.677.686zM22.874 19.77c0 2.517 8.146 1.879 8.146 6.663 0 2.292-1.783 4.25-4.681 4.657v1.897c0 .265-.237.48-.53.48h-1.763c-.292 0-.53-.215-.53-.48v-1.928c-1.74-.268-3.293-.997-4.353-1.917a.447.447 0 0 1-.058-.634l1.34-1.625a.566.566 0 0 1 .757-.086c1.098.8 2.517 1.435 3.873 1.435 1.58 0 2.299-.853 2.299-1.646 0-2.342-8.147-1.834-8.147-6.772 0-2.107 1.705-3.811 4.29-4.342V13.48c0-.265.237-.48.53-.48h1.763c.292 0 .529.215.529.48v1.888c1.418.149 2.954.653 4.025 1.511.184.148.23.391.113.587l-1.038 1.74c-.152.255-.516.33-.775.161-.985-.642-2.193-1.132-3.371-1.132-1.47 0-2.42.603-2.42 1.535z"/><path id="e" d="M19.25 32.621C20.983 33.541 22.9 34 25 34c4 0 7-1.667 9-5h15.036v-3.212a.342.342 0 0 0-.339-.343H35c.347-.502.423-1.416.228-2.742h13.808c1.5 0 2.714 1.228 2.714 2.742v20.11c0 1.514-1.213 2.742-2.714 2.742H21.964c-1.5 0-2.714-1.228-2.714-2.742V32.62zm29.447 12.934a.342.342 0 0 0 .339-.343V35.5H21.964v9.712c0 .188.152.343.339.343h26.394zm-18.614-5.713v2.285a.683.683 0 0 1-.677.685h-4.062a.683.683 0 0 1-.677-.685v-2.285c0-.377.304-.686.677-.686h4.062c.373 0 .677.309.677.686zm10.834 0v2.285a.683.683 0 0 1-.677.685h-7.674a.683.683 0 0 1-.677-.685v-2.285c0-.377.305-.686.677-.686h7.674c.372 0 .677.309.677.686zM22.874 17.77c0 2.517 8.146 1.879 8.146 6.663 0 2.292-1.783 4.25-4.681 4.657v1.897c0 .265-.237.48-.53.48h-1.763c-.292 0-.53-.215-.53-.48v-1.928c-1.74-.268-3.293-.997-4.353-1.917a.447.447 0 0 1-.058-.634l1.34-1.625a.566.566 0 0 1 .757-.086c1.098.8 2.517 1.435 3.873 1.435 1.58 0 2.299-.853 2.299-1.646 0-2.342-8.147-1.834-8.147-6.772 0-2.107 1.705-3.811 4.29-4.342V11.48c0-.265.237-.48.53-.48h1.763c.292 0 .529.215.529.48v1.888c1.418.149 2.954.653 4.025 1.511.184.148.23.391.113.587l-1.038 1.74c-.152.255-.516.33-.775.161-.985-.642-2.193-1.132-3.371-1.132-1.47 0-2.42.603-2.42 1.535z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M4 69c-2 0-4-1-4-4V36.095l23.6-24.837 5.552 6.125-3.147 3.581 4.02 6.176 5.257-4.413L48 23l3.377 2.017v21.868L32.142 69H4z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#d"/><use fill="#FFF" fill-rule="nonzero" xlink:href="#e"/></g></g></svg> \ No newline at end of file
diff --git a/addons/payment/static/img/american_express.png b/addons/payment/static/img/american_express.png
new file mode 100644
index 00000000..9f31d21e
--- /dev/null
+++ b/addons/payment/static/img/american_express.png
Binary files differ
diff --git a/addons/payment/static/img/bancontact.png b/addons/payment/static/img/bancontact.png
new file mode 100644
index 00000000..7941a69c
--- /dev/null
+++ b/addons/payment/static/img/bancontact.png
Binary files differ
diff --git a/addons/payment/static/img/cirrus.png b/addons/payment/static/img/cirrus.png
new file mode 100644
index 00000000..eb4b2aa8
--- /dev/null
+++ b/addons/payment/static/img/cirrus.png
Binary files differ
diff --git a/addons/payment/static/img/codensa_easy_credit.png b/addons/payment/static/img/codensa_easy_credit.png
new file mode 100644
index 00000000..7f5d4875
--- /dev/null
+++ b/addons/payment/static/img/codensa_easy_credit.png
Binary files differ
diff --git a/addons/payment/static/img/diners_club_intl.png b/addons/payment/static/img/diners_club_intl.png
new file mode 100644
index 00000000..888860a1
--- /dev/null
+++ b/addons/payment/static/img/diners_club_intl.png
Binary files differ
diff --git a/addons/payment/static/img/discover.png b/addons/payment/static/img/discover.png
new file mode 100644
index 00000000..d9222265
--- /dev/null
+++ b/addons/payment/static/img/discover.png
Binary files differ
diff --git a/addons/payment/static/img/eps.png b/addons/payment/static/img/eps.png
new file mode 100644
index 00000000..71ca1305
--- /dev/null
+++ b/addons/payment/static/img/eps.png
Binary files differ
diff --git a/addons/payment/static/img/giropay.png b/addons/payment/static/img/giropay.png
new file mode 100644
index 00000000..8869385f
--- /dev/null
+++ b/addons/payment/static/img/giropay.png
Binary files differ
diff --git a/addons/payment/static/img/ideal.png b/addons/payment/static/img/ideal.png
new file mode 100644
index 00000000..e506097c
--- /dev/null
+++ b/addons/payment/static/img/ideal.png
Binary files differ
diff --git a/addons/payment/static/img/jcb.png b/addons/payment/static/img/jcb.png
new file mode 100644
index 00000000..f3e3c5bf
--- /dev/null
+++ b/addons/payment/static/img/jcb.png
Binary files differ
diff --git a/addons/payment/static/img/maestro.png b/addons/payment/static/img/maestro.png
new file mode 100644
index 00000000..a0a6b328
--- /dev/null
+++ b/addons/payment/static/img/maestro.png
Binary files differ
diff --git a/addons/payment/static/img/mastercard.png b/addons/payment/static/img/mastercard.png
new file mode 100644
index 00000000..2572a33b
--- /dev/null
+++ b/addons/payment/static/img/mastercard.png
Binary files differ
diff --git a/addons/payment/static/img/p24.png b/addons/payment/static/img/p24.png
new file mode 100644
index 00000000..5ed3f7bf
--- /dev/null
+++ b/addons/payment/static/img/p24.png
Binary files differ
diff --git a/addons/payment/static/img/unionpay.png b/addons/payment/static/img/unionpay.png
new file mode 100644
index 00000000..8d830c5f
--- /dev/null
+++ b/addons/payment/static/img/unionpay.png
Binary files differ
diff --git a/addons/payment/static/img/visa.png b/addons/payment/static/img/visa.png
new file mode 100644
index 00000000..58cfab08
--- /dev/null
+++ b/addons/payment/static/img/visa.png
Binary files differ
diff --git a/addons/payment/static/img/webmoney.png b/addons/payment/static/img/webmoney.png
new file mode 100644
index 00000000..2e4e5916
--- /dev/null
+++ b/addons/payment/static/img/webmoney.png
Binary files differ
diff --git a/addons/payment/static/img/western_union.png b/addons/payment/static/img/western_union.png
new file mode 100644
index 00000000..02969bd5
--- /dev/null
+++ b/addons/payment/static/img/western_union.png
Binary files differ
diff --git a/addons/payment/static/lib/jquery.payment/jquery.payment.js b/addons/payment/static/lib/jquery.payment/jquery.payment.js
new file mode 100644
index 00000000..dcf829fb
--- /dev/null
+++ b/addons/payment/static/lib/jquery.payment/jquery.payment.js
@@ -0,0 +1,652 @@
+// Generated by CoffeeScript 1.7.1
+(function() {
+ var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlashAndSpace, hasTextSelected, luhnCheck, reFormatCVC, reFormatCardNumber, reFormatExpiry, reFormatNumeric, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, safeVal, setCardType,
+ __slice = [].slice,
+ __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+
+ $ = window.jQuery || window.Zepto || window.$;
+
+ $.payment = {};
+
+ $.payment.fn = {};
+
+ $.fn.payment = function() {
+ var args, method;
+ method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+ return $.payment.fn[method].apply(this, args);
+ };
+
+ defaultFormat = /(\d{1,4})/g;
+
+ $.payment.cards = cards = [
+ {
+ type: 'maestro',
+ patterns: [5018, 502, 503, 506, 56, 58, 639, 6220, 67],
+ format: defaultFormat,
+ length: [12, 13, 14, 15, 16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'forbrugsforeningen',
+ patterns: [600],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'dankort',
+ patterns: [5019],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'visa',
+ patterns: [4],
+ format: defaultFormat,
+ length: [13, 16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'mastercard',
+ patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'amex',
+ patterns: [34, 37],
+ format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
+ length: [15],
+ cvcLength: [3, 4],
+ luhn: true
+ }, {
+ type: 'dinersclub',
+ patterns: [30, 36, 38, 39],
+ format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/,
+ length: [14],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'discover',
+ patterns: [60, 64, 65, 622],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'unionpay',
+ patterns: [62, 88],
+ format: defaultFormat,
+ length: [16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: false
+ }, {
+ type: 'jcb',
+ patterns: [35],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }
+ ];
+
+ cardFromNumber = function(num) {
+ var card, p, pattern, _i, _j, _len, _len1, _ref;
+ num = (num + '').replace(/\D/g, '');
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ _ref = card.patterns;
+ for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
+ pattern = _ref[_j];
+ p = pattern + '';
+ if (num.substr(0, p.length) === p) {
+ return card;
+ }
+ }
+ }
+ };
+
+ cardFromType = function(type) {
+ var card, _i, _len;
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ if (card.type === type) {
+ return card;
+ }
+ }
+ };
+
+ luhnCheck = function(num) {
+ var digit, digits, odd, sum, _i, _len;
+ odd = true;
+ sum = 0;
+ digits = (num + '').split('').reverse();
+ for (_i = 0, _len = digits.length; _i < _len; _i++) {
+ digit = digits[_i];
+ digit = parseInt(digit, 10);
+ if ((odd = !odd)) {
+ digit *= 2;
+ }
+ if (digit > 9) {
+ digit -= 9;
+ }
+ sum += digit;
+ }
+ return sum % 10 === 0;
+ };
+
+ hasTextSelected = function($target) {
+ var _ref;
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) {
+ return true;
+ }
+ if ((typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? _ref.createRange : void 0 : void 0) != null) {
+ if (document.selection.createRange().text) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ safeVal = function(value, $target) {
+ var currPair, cursor, digit, error, last, prevPair;
+ try {
+ cursor = $target.prop('selectionStart');
+ } catch (_error) {
+ error = _error;
+ cursor = null;
+ }
+ last = $target.val();
+ $target.val(value);
+ if (cursor !== null && $target.is(":focus")) {
+ if (cursor === last.length) {
+ cursor = value.length;
+ }
+ if (last !== value) {
+ prevPair = last.slice(cursor - 1, +cursor + 1 || 9e9);
+ currPair = value.slice(cursor - 1, +cursor + 1 || 9e9);
+ digit = value[cursor];
+ if (/\d/.test(digit) && prevPair === ("" + digit + " ") && currPair === (" " + digit)) {
+ cursor = cursor + 1;
+ }
+ }
+ $target.prop('selectionStart', cursor);
+ return $target.prop('selectionEnd', cursor);
+ }
+ };
+
+ replaceFullWidthChars = function(str) {
+ var chars, chr, fullWidth, halfWidth, idx, value, _i, _len;
+ if (str == null) {
+ str = '';
+ }
+ fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19';
+ halfWidth = '0123456789';
+ value = '';
+ chars = str.split('');
+ for (_i = 0, _len = chars.length; _i < _len; _i++) {
+ chr = chars[_i];
+ idx = fullWidth.indexOf(chr);
+ if (idx > -1) {
+ chr = halfWidth[idx];
+ }
+ value += chr;
+ }
+ return value;
+ };
+
+ reFormatNumeric = function(e) {
+ var $target;
+ $target = $(e.currentTarget);
+ return setTimeout(function() {
+ var value;
+ value = $target.val();
+ value = replaceFullWidthChars(value);
+ value = value.replace(/\D/g, '');
+ return safeVal(value, $target);
+ });
+ };
+
+ reFormatCardNumber = function(e) {
+ var $target;
+ $target = $(e.currentTarget);
+ return setTimeout(function() {
+ var value;
+ value = $target.val();
+ value = replaceFullWidthChars(value);
+ value = $.payment.formatCardNumber(value);
+ return safeVal(value, $target);
+ });
+ };
+
+ formatCardNumber = function(e) {
+ var $target, card, digit, length, re, upperLength, value;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ value = $target.val();
+ card = cardFromNumber(value + digit);
+ length = (value.replace(/\D/g, '') + digit).length;
+ upperLength = 16;
+ if (card) {
+ upperLength = card.length[card.length.length - 1];
+ }
+ if (length >= upperLength) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (card && card.type === 'amex') {
+ re = /^(\d{4}|\d{4}\s\d{6})$/;
+ } else {
+ re = /(?:^|\s)(\d{4})$/;
+ }
+ if (re.test(value)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value + ' ' + digit);
+ });
+ } else if (re.test(value + digit)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value + digit + ' ');
+ });
+ }
+ };
+
+ formatBackCardNumber = function(e) {
+ var $target, value;
+ $target = $(e.currentTarget);
+ value = $target.val();
+ if (e.which !== 8) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (/\d\s$/.test(value)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value.replace(/\d\s$/, ''));
+ });
+ } else if (/\s\d?$/.test(value)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value.replace(/\d$/, ''));
+ });
+ }
+ };
+
+ reFormatExpiry = function(e) {
+ var $target;
+ $target = $(e.currentTarget);
+ return setTimeout(function() {
+ var value;
+ value = $target.val();
+ value = replaceFullWidthChars(value);
+ value = $.payment.formatExpiry(value);
+ return safeVal(value, $target);
+ });
+ };
+
+ formatExpiry = function(e) {
+ var $target, digit, val;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val() + digit;
+ if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val("0" + val + " / ");
+ });
+ } else if (/^\d\d$/.test(val)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ var m1, m2;
+ m1 = parseInt(val[0], 10);
+ m2 = parseInt(val[1], 10);
+ if (m2 > 2 && m1 !== 0) {
+ return $target.val("0" + m1 + " / " + m2);
+ } else {
+ return $target.val("" + val + " / ");
+ }
+ });
+ }
+ };
+
+ formatForwardExpiry = function(e) {
+ var $target, digit, val;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val();
+ if (/^\d\d$/.test(val)) {
+ return $target.val("" + val + " / ");
+ }
+ };
+
+ formatForwardSlashAndSpace = function(e) {
+ var $target, val, which;
+ which = String.fromCharCode(e.which);
+ if (!(which === '/' || which === ' ')) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val();
+ if (/^\d$/.test(val) && val !== '0') {
+ return $target.val("0" + val + " / ");
+ }
+ };
+
+ formatBackExpiry = function(e) {
+ var $target, value;
+ $target = $(e.currentTarget);
+ value = $target.val();
+ if (e.which !== 8) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (/\d\s\/\s$/.test(value)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value.replace(/\d\s\/\s$/, ''));
+ });
+ }
+ };
+
+ reFormatCVC = function(e) {
+ var $target;
+ $target = $(e.currentTarget);
+ return setTimeout(function() {
+ var value;
+ value = $target.val();
+ value = replaceFullWidthChars(value);
+ value = value.replace(/\D/g, '').slice(0, 4);
+ return safeVal(value, $target);
+ });
+ };
+
+ restrictNumeric = function(e) {
+ var input;
+ if (e.metaKey || e.ctrlKey) {
+ return true;
+ }
+ if (e.which === 32) {
+ return false;
+ }
+ if (e.which === 0) {
+ return true;
+ }
+ if (e.which < 33) {
+ return true;
+ }
+ input = String.fromCharCode(e.which);
+ return !!/[\d\s]/.test(input);
+ };
+
+ restrictCardNumber = function(e) {
+ var $target, card, digit, value;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ value = ($target.val() + digit).replace(/\D/g, '');
+ card = cardFromNumber(value);
+ if (card) {
+ return value.length <= card.length[card.length.length - 1];
+ } else {
+ return value.length <= 16;
+ }
+ };
+
+ restrictExpiry = function(e) {
+ var $target, digit, value;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ value = $target.val() + digit;
+ value = value.replace(/\D/g, '');
+ if (value.length > 6) {
+ return false;
+ }
+ };
+
+ restrictCVC = function(e) {
+ var $target, digit, val;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ val = $target.val() + digit;
+ return val.length <= 4;
+ };
+
+ setCardType = function(e) {
+ var $target, allTypes, card, cardType, val;
+ $target = $(e.currentTarget);
+ val = $target.val();
+ cardType = $.payment.cardType(val) || 'unknown';
+ if (!$target.hasClass(cardType)) {
+ allTypes = (function() {
+ var _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ _results.push(card.type);
+ }
+ return _results;
+ })();
+ $target.removeClass('unknown');
+ $target.removeClass(allTypes.join(' '));
+ $target.addClass(cardType);
+ $target.toggleClass('identified', cardType !== 'unknown');
+ return $target.trigger('payment.cardType', cardType);
+ }
+ };
+
+ $.payment.fn.formatCardCVC = function() {
+ this.on('keypress', restrictNumeric);
+ this.on('keypress', restrictCVC);
+ this.on('paste', reFormatCVC);
+ this.on('change', reFormatCVC);
+ this.on('input', reFormatCVC);
+ return this;
+ };
+
+ $.payment.fn.formatCardExpiry = function() {
+ this.on('keypress', restrictNumeric);
+ this.on('keypress', restrictExpiry);
+ this.on('keypress', formatExpiry);
+ this.on('keypress', formatForwardSlashAndSpace);
+ this.on('keypress', formatForwardExpiry);
+ this.on('keydown', formatBackExpiry);
+ this.on('change', reFormatExpiry);
+ this.on('input', reFormatExpiry);
+ return this;
+ };
+
+ $.payment.fn.formatCardNumber = function() {
+ this.on('keypress', restrictNumeric);
+ this.on('keypress', restrictCardNumber);
+ this.on('keypress', formatCardNumber);
+ this.on('keydown', formatBackCardNumber);
+ this.on('keyup', setCardType);
+ this.on('paste', reFormatCardNumber);
+ this.on('change', reFormatCardNumber);
+ this.on('input', reFormatCardNumber);
+ this.on('input', setCardType);
+ return this;
+ };
+
+ $.payment.fn.restrictNumeric = function() {
+ this.on('keypress', restrictNumeric);
+ this.on('paste', reFormatNumeric);
+ this.on('change', reFormatNumeric);
+ this.on('input', reFormatNumeric);
+ return this;
+ };
+
+ $.payment.fn.cardExpiryVal = function() {
+ return $.payment.cardExpiryVal($(this).val());
+ };
+
+ $.payment.cardExpiryVal = function(value) {
+ var month, prefix, year, _ref;
+ _ref = value.split(/[\s\/]+/, 2), month = _ref[0], year = _ref[1];
+ if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) {
+ prefix = (new Date).getFullYear();
+ prefix = prefix.toString().slice(0, 2);
+ year = prefix + year;
+ }
+ month = parseInt(month, 10);
+ year = parseInt(year, 10);
+ return {
+ month: month,
+ year: year
+ };
+ };
+
+ $.payment.validateCardNumber = function(num) {
+ var card, _ref;
+ num = (num + '').replace(/\s+|-/g, '');
+ if (!/^\d+$/.test(num)) {
+ return false;
+ }
+ card = cardFromNumber(num);
+ if (!card) {
+ return false;
+ }
+ return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num));
+ };
+
+ $.payment.validateCardExpiry = function(month, year) {
+ var currentTime, expiry, _ref;
+ if (typeof month === 'object' && 'month' in month) {
+ _ref = month, month = _ref.month, year = _ref.year;
+ }
+ if (!(month && year)) {
+ return false;
+ }
+ month = $.trim(month);
+ year = $.trim(year);
+ if (!/^\d+$/.test(month)) {
+ return false;
+ }
+ if (!/^\d+$/.test(year)) {
+ return false;
+ }
+ if (!((1 <= month && month <= 12))) {
+ return false;
+ }
+ if (year.length === 2) {
+ if (year < 70) {
+ year = "20" + year;
+ } else {
+ year = "19" + year;
+ }
+ }
+ if (year.length !== 4) {
+ return false;
+ }
+ expiry = new Date(year, month);
+ currentTime = new Date;
+ expiry.setMonth(expiry.getMonth() - 1);
+ expiry.setMonth(expiry.getMonth() + 1, 1);
+ return expiry > currentTime;
+ };
+
+ $.payment.validateCardCVC = function(cvc, type) {
+ var card, _ref;
+ cvc = $.trim(cvc);
+ if (!/^\d+$/.test(cvc)) {
+ return false;
+ }
+ card = cardFromType(type);
+ if (card != null) {
+ return _ref = cvc.length, __indexOf.call(card.cvcLength, _ref) >= 0;
+ } else {
+ return cvc.length >= 3 && cvc.length <= 4;
+ }
+ };
+
+ $.payment.cardType = function(num) {
+ var _ref;
+ if (!num) {
+ return null;
+ }
+ return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null;
+ };
+
+ $.payment.formatCardNumber = function(num) {
+ var card, groups, upperLength, _ref;
+ num = num.replace(/\D/g, '');
+ card = cardFromNumber(num);
+ if (!card) {
+ return num;
+ }
+ upperLength = card.length[card.length.length - 1];
+ num = num.slice(0, upperLength);
+ if (card.format.global) {
+ return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0;
+ } else {
+ groups = card.format.exec(num);
+ if (groups == null) {
+ return;
+ }
+ groups.shift();
+ groups = $.grep(groups, function(n) {
+ return n;
+ });
+ return groups.join(' ');
+ }
+ };
+
+ $.payment.formatExpiry = function(expiry) {
+ var mon, parts, sep, year;
+ parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/);
+ if (!parts) {
+ return '';
+ }
+ mon = parts[1] || '';
+ sep = parts[2] || '';
+ year = parts[3] || '';
+ if (year.length > 0) {
+ sep = ' / ';
+ } else if (sep === ' /') {
+ mon = mon.substring(0, 1);
+ sep = '';
+ } else if (mon.length === 2 || sep.length > 0) {
+ sep = ' / ';
+ } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) {
+ mon = "0" + mon;
+ sep = ' / ';
+ }
+ return mon + sep + year;
+ };
+
+}).call(this);
diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js
new file mode 100644
index 00000000..e74dfe3e
--- /dev/null
+++ b/addons/payment/static/src/js/payment_form.js
@@ -0,0 +1,588 @@
+odoo.define('payment.payment_form', function (require) {
+"use strict";
+
+var core = require('web.core');
+var Dialog = require('web.Dialog');
+var publicWidget = require('web.public.widget');
+
+var _t = core._t;
+
+publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
+ selector: '.o_payment_form',
+ events: {
+ 'submit': 'onSubmit',
+ 'click #o_payment_form_pay': 'payEvent',
+ 'click #o_payment_form_add_pm': 'addPmEvent',
+ 'click button[name="delete_pm"]': 'deletePmEvent',
+ 'click .o_payment_form_pay_icon_more': 'onClickMorePaymentIcon',
+ 'click .o_payment_acquirer_select': 'radioClickEvent',
+ },
+
+ /**
+ * @override
+ */
+ start: function () {
+ this._adaptPayButton();
+ window.addEventListener('pageshow', function (event) {
+ if (event.persisted) {
+ window.location.reload();
+ }
+ });
+ var self = this;
+ return this._super.apply(this, arguments).then(function () {
+ self.options = _.extend(self.$el.data(), self.options);
+ self.updateNewPaymentDisplayStatus();
+ $('[data-toggle="tooltip"]').tooltip();
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {string} title
+ * @param {string} message
+ */
+ displayError: function (title, message) {
+ var $checkedRadio = this.$('input[type="radio"]:checked'),
+ acquirerID = this.getAcquirerIdFromRadio($checkedRadio[0]);
+ var $acquirerForm;
+ if (this.isNewPaymentRadio($checkedRadio[0])) {
+ $acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID);
+ }
+ else if (this.isFormPaymentRadio($checkedRadio[0])) {
+ $acquirerForm = this.$('#o_payment_form_acq_' + acquirerID);
+ }
+
+ if ($checkedRadio.length === 0) {
+ return new Dialog(null, {
+ title: _t('Error: ') + _.str.escapeHTML(title),
+ size: 'medium',
+ $content: "<p>" + (_.str.escapeHTML(message) || "") + "</p>" ,
+ buttons: [
+ {text: _t('Ok'), close: true}]}).open();
+ } else {
+ // removed if exist error message
+ this.$('#payment_error').remove();
+ var messageResult = '<div class="alert alert-danger mb4" id="payment_error">';
+ if (title != '') {
+ messageResult = messageResult + '<b>' + _.str.escapeHTML(title) + ':</b><br/>';
+ }
+ messageResult = messageResult + _.str.escapeHTML(message) + '</div>';
+ $acquirerForm.append(messageResult);
+ }
+ },
+ hideError: function() {
+ this.$('#payment_error').remove();
+ },
+ /**
+ * @private
+ * @param {DOMElement} element
+ */
+ getAcquirerIdFromRadio: function (element) {
+ return $(element).data('acquirer-id');
+ },
+ /**
+ * @private
+ * @param {jQuery} $form
+ */
+ getFormData: function ($form) {
+ var unindexed_array = $form.serializeArray();
+ var indexed_array = {};
+
+ $.map(unindexed_array, function (n, i) {
+ indexed_array[n.name] = n.value;
+ });
+ return indexed_array;
+ },
+ /**
+ * @private
+ * @param {DOMElement} element
+ */
+ isFormPaymentRadio: function (element) {
+ return $(element).data('form-payment') === 'True';
+ },
+ /**
+ * @private
+ * @param {DOMElement} element
+ */
+ isNewPaymentRadio: function (element) {
+ return $(element).data('s2s-payment') === 'True';
+ },
+ /**
+ * @private
+ */
+ updateNewPaymentDisplayStatus: function () {
+ var checked_radio = this.$('input[type="radio"]:checked');
+ // we hide all the acquirers form
+ this.$('[id*="o_payment_add_token_acq_"]').addClass('d-none');
+ this.$('[id*="o_payment_form_acq_"]').addClass('d-none');
+ if (checked_radio.length !== 1) {
+ return;
+ }
+ checked_radio = checked_radio[0];
+ var acquirer_id = this.getAcquirerIdFromRadio(checked_radio);
+
+ // if we clicked on an add new payment radio, display its form
+ if (this.isNewPaymentRadio(checked_radio)) {
+ this.$('#o_payment_add_token_acq_' + acquirer_id).removeClass('d-none');
+ }
+ else if (this.isFormPaymentRadio(checked_radio)) {
+ this.$('#o_payment_form_acq_' + acquirer_id).removeClass('d-none');
+ }
+ },
+
+ disableButton: function (button) {
+ $("body").block({overlayCSS: {backgroundColor: "#000", opacity: 0, zIndex: 1050}, message: false});
+ $(button).attr('disabled', true);
+ $(button).children('.fa-lock').removeClass('fa-lock');
+ $(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
+ },
+
+ enableButton: function (button) {
+ $('body').unblock();
+ $(button).attr('disabled', false);
+ $(button).children('.fa').addClass('fa-lock');
+ $(button).find('span.o_loader').remove();
+ },
+ _parseError: function(e) {
+ if (e.message.data.arguments[1]) {
+ return e.message.data.arguments[0] + e.message.data.arguments[1];
+ }
+ return e.message.data.arguments[0];
+ },
+ _adaptPayButton: function () {
+ var $payButton = $("#o_payment_form_pay");
+ var disabledReasons = $payButton.data('disabled_reasons') || {};
+ $payButton.prop('disabled', _.contains(disabledReasons, true));
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ payEvent: function (ev) {
+ ev.preventDefault();
+ var form = this.el;
+ var checked_radio = this.$('input[type="radio"]:checked');
+ var self = this;
+ if (ev.type === 'submit') {
+ var button = $(ev.target).find('*[type="submit"]')[0]
+ } else {
+ var button = ev.target;
+ }
+
+ // first we check that the user has selected a payment method
+ if (checked_radio.length === 1) {
+ checked_radio = checked_radio[0];
+
+ // we retrieve all the input inside the acquirer form and 'serialize' them to an indexed array
+ var acquirer_id = this.getAcquirerIdFromRadio(checked_radio);
+ var acquirer_form = false;
+ if (this.isNewPaymentRadio(checked_radio)) {
+ acquirer_form = this.$('#o_payment_add_token_acq_' + acquirer_id);
+ } else {
+ acquirer_form = this.$('#o_payment_form_acq_' + acquirer_id);
+ }
+ var inputs_form = $('input', acquirer_form);
+ var ds = $('input[name="data_set"]', acquirer_form)[0];
+
+ // if the user is adding a new payment
+ if (this.isNewPaymentRadio(checked_radio)) {
+ if (this.options.partnerId === undefined) {
+ console.warn('payment_form: unset partner_id when adding new token; things could go wrong');
+ }
+ var form_data = this.getFormData(inputs_form);
+ var wrong_input = false;
+
+ inputs_form.toArray().forEach(function (element) {
+ //skip the check of non visible inputs
+ if ($(element).attr('type') == 'hidden') {
+ return true;
+ }
+ $(element).closest('div.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
+ $(element).siblings( ".o_invalid_field" ).remove();
+ //force check of forms validity (useful for Firefox that refill forms automatically on f5)
+ $(element).trigger("focusout");
+ if (element.dataset.isRequired && element.value.length === 0) {
+ $(element).closest('div.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
+ $(element).closest('div.form-group').append('<div style="color: red" class="o_invalid_field" aria-invalid="true">' + _.str.escapeHTML("The value is invalid.") + '</div>');
+ wrong_input = true;
+ }
+ else if ($(element).closest('div.form-group').hasClass('o_has_error')) {
+ wrong_input = true;
+ $(element).closest('div.form-group').append('<div style="color: red" class="o_invalid_field" aria-invalid="true">' + _.str.escapeHTML("The value is invalid.") + '</div>');
+ }
+ });
+
+ if (wrong_input) {
+ return;
+ }
+
+ this.disableButton(button);
+ // do the call to the route stored in the 'data_set' input of the acquirer form, the data must be called 'create-route'
+ return this._rpc({
+ route: ds.dataset.createRoute,
+ params: form_data,
+ }).then(function (data) {
+ // if the server has returned true
+ if (data.result) {
+ // and it need a 3DS authentication
+ if (data['3d_secure'] !== false) {
+ // then we display the 3DS page to the user
+ $("body").html(data['3d_secure']);
+ }
+ else {
+ checked_radio.value = data.id; // set the radio value to the new card id
+ form.submit();
+ return new Promise(function () {});
+ }
+ }
+ // if the server has returned false, we display an error
+ else {
+ if (data.error) {
+ self.displayError(
+ '',
+ data.error);
+ } else { // if the server doesn't provide an error message
+ self.displayError(
+ _t('Server Error'),
+ _t('e.g. Your credit card details are wrong. Please verify.'));
+ }
+ }
+ // here we remove the 'processing' icon from the 'add a new payment' button
+ self.enableButton(button);
+ }).guardedCatch(function (error) {
+ error.event.preventDefault();
+ // if the rpc fails, pretty obvious
+ self.enableButton(button);
+
+ self.displayError(
+ _t('Server Error'),
+ _t("We are not able to add your payment method at the moment.") +
+ self._parseError(error)
+ );
+ });
+ }
+ // if the user is going to pay with a form payment, then
+ else if (this.isFormPaymentRadio(checked_radio)) {
+ this.disableButton(button);
+ var $tx_url = this.$el.find('input[name="prepare_tx_url"]');
+ // if there's a prepare tx url set
+ if ($tx_url.length === 1) {
+ // if the user wants to save his credit card info
+ var form_save_token = acquirer_form.find('input[name="o_payment_form_save_token"]').prop('checked');
+ // then we call the route to prepare the transaction
+ return this._rpc({
+ route: $tx_url[0].value,
+ params: {
+ 'acquirer_id': parseInt(acquirer_id),
+ 'save_token': form_save_token,
+ 'access_token': self.options.accessToken,
+ 'success_url': self.options.successUrl,
+ 'error_url': self.options.errorUrl,
+ 'callback_method': self.options.callbackMethod,
+ 'order_id': self.options.orderId,
+ 'invoice_id': self.options.invoiceId,
+ },
+ }).then(function (result) {
+ if (result) {
+ // if the server sent us the html form, we create a form element
+ var newForm = document.createElement('form');
+ newForm.setAttribute("method", self._get_redirect_form_method());
+ newForm.setAttribute("provider", checked_radio.dataset.provider);
+ newForm.hidden = true; // hide it
+ newForm.innerHTML = result; // put the html sent by the server inside the form
+ var action_url = $(newForm).find('input[name="data_set"]').data('actionUrl');
+ newForm.setAttribute("action", action_url); // set the action url
+ $(document.getElementsByTagName('body')[0]).append(newForm); // append the form to the body
+ $(newForm).find('input[data-remove-me]').remove(); // remove all the input that should be removed
+ if(action_url) {
+ newForm.submit(); // and finally submit the form
+ return new Promise(function () {});
+ }
+ }
+ else {
+ self.displayError(
+ _t('Server Error'),
+ _t("We are not able to redirect you to the payment form.")
+ );
+ self.enableButton(button);
+ }
+ }).guardedCatch(function (error) {
+ error.event.preventDefault();
+ self.displayError(
+ _t('Server Error'),
+ _t("We are not able to redirect you to the payment form.") + " " +
+ self._parseError(error)
+ );
+ self.enableButton(button);
+ });
+ }
+ else {
+ // we append the form to the body and send it.
+ this.displayError(
+ _t("Cannot setup the payment"),
+ _t("We're unable to process your payment.")
+ );
+ self.enableButton(button);
+ }
+ }
+ else { // if the user is using an old payment then we just submit the form
+ this.disableButton(button);
+ form.submit();
+ return new Promise(function () {});
+ }
+ }
+ else {
+ this.displayError(
+ _t('No payment method selected'),
+ _t('Please select a payment method.')
+ );
+ this.enableButton(button);
+ }
+ },
+ /**
+ * Return the HTTP method to be used by the redirect form.
+ *
+ * @private
+ * @return {string} The HTTP method, "post" by default
+ */
+ _get_redirect_form_method: function () {
+ return "post";
+ },
+ /**
+ * Called when clicking on the button to add a new payment method.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ addPmEvent: function (ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ var checked_radio = this.$('input[type="radio"]:checked');
+ var self = this;
+ if (ev.type === 'submit') {
+ var button = $(ev.target).find('*[type="submit"]')[0]
+ } else {
+ var button = ev.target;
+ }
+
+ // we check if the user has selected a 'add a new payment' option
+ if (checked_radio.length === 1 && this.isNewPaymentRadio(checked_radio[0])) {
+ // we retrieve which acquirer is used
+ checked_radio = checked_radio[0];
+ var acquirer_id = this.getAcquirerIdFromRadio(checked_radio);
+ var acquirer_form = this.$('#o_payment_add_token_acq_' + acquirer_id);
+ // we retrieve all the input inside the acquirer form and 'serialize' them to an indexed array
+ var inputs_form = $('input', acquirer_form);
+ var form_data = this.getFormData(inputs_form);
+ var ds = $('input[name="data_set"]', acquirer_form)[0];
+ var wrong_input = false;
+
+ inputs_form.toArray().forEach(function (element) {
+ //skip the check of non visible inputs
+ if ($(element).attr('type') == 'hidden') {
+ return true;
+ }
+ $(element).closest('div.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
+ $(element).siblings( ".o_invalid_field" ).remove();
+ //force check of forms validity (useful for Firefox that refill forms automatically on f5)
+ $(element).trigger("focusout");
+ if (element.dataset.isRequired && element.value.length === 0) {
+ $(element).closest('div.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
+ var message = '<div style="color: red" class="o_invalid_field" aria-invalid="true">' + _.str.escapeHTML("The value is invalid.") + '</div>';
+ $(element).closest('div.form-group').append(message);
+ wrong_input = true;
+ }
+ else if ($(element).closest('div.form-group').hasClass('o_has_error')) {
+ wrong_input = true;
+ var message = '<div style="color: red" class="o_invalid_field" aria-invalid="true">' + _.str.escapeHTML("The value is invalid.") + '</div>';
+ $(element).closest('div.form-group').append(message);
+ }
+ });
+
+ if (wrong_input) {
+ return;
+ }
+ // We add a 'processing' icon into the 'add a new payment' button
+ $(button).attr('disabled', true);
+ $(button).children('.fa-plus-circle').removeClass('fa-plus-circle');
+ $(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
+
+ // do the call to the route stored in the 'data_set' input of the acquirer form, the data must be called 'create-route'
+ this._rpc({
+ route: ds.dataset.createRoute,
+ params: form_data,
+ }).then(function (data) {
+ // if the server has returned true
+ if (data.result) {
+ // and it need a 3DS authentication
+ if (data['3d_secure'] !== false) {
+ // then we display the 3DS page to the user
+ $("body").html(data['3d_secure']);
+ }
+ // if it doesn't require 3DS
+ else {
+ // we just go to the return_url or reload the page
+ if (form_data.return_url) {
+ window.location = form_data.return_url;
+ }
+ else {
+ window.location.reload();
+ }
+ }
+ }
+ // if the server has returned false, we display an error
+ else {
+ if (data.error) {
+ self.displayError(
+ '',
+ data.error);
+ } else { // if the server doesn't provide an error message
+ self.displayError(
+ _t('Server Error'),
+ _t('e.g. Your credit card details are wrong. Please verify.'));
+ }
+ }
+ // here we remove the 'processing' icon from the 'add a new payment' button
+ $(button).attr('disabled', false);
+ $(button).children('.fa').addClass('fa-plus-circle');
+ $(button).find('span.o_loader').remove();
+ }).guardedCatch(function (error) {
+ error.event.preventDefault();
+ // if the rpc fails, pretty obvious
+ $(button).attr('disabled', false);
+ $(button).children('.fa').addClass('fa-plus-circle');
+ $(button).find('span.o_loader').remove();
+
+ self.displayError(
+ _t('Server error'),
+ _t("We are not able to add your payment method at the moment.") +
+ self._parseError(error)
+ );
+ });
+ }
+ else {
+ this.displayError(
+ _t('No payment method selected'),
+ _t('Please select the option to add a new payment method.')
+ );
+ }
+ },
+ /**
+ * Called when submitting the form (e.g. through the Return key).
+ * We need to check whether we are paying or adding a new pm and dispatch
+ * to the correct method.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ onSubmit: function(ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ var button = $(ev.target).find('*[type="submit"]')[0]
+ if (button.id === 'o_payment_form_pay') {
+ return this.payEvent(ev);
+ } else if (button.id === 'o_payment_form_add_pm') {
+ return this.addPmEvent(ev);
+ }
+ return;
+ },
+ /**
+ * Called when clicking on a button to delete a payment method.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ deletePmEvent: function (ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ var self = this;
+ var pm_id = parseInt(ev.target.value);
+
+ var tokenDelete = function () {
+ self._rpc({
+ model: 'payment.token',
+ method: 'unlink',
+ args: [pm_id],
+ }).then(function (result) {
+ if (result === true) {
+ ev.target.closest('div').remove();
+ }
+ }, function () {
+ self.displayError(
+ _t('Server Error'),
+ _t("We are not able to delete your payment method at the moment.")
+ );
+ });
+ };
+
+ this._rpc({
+ model: 'payment.token',
+ method: 'get_linked_records',
+ args: [pm_id],
+ }).then(function (result) {
+ if (result[pm_id].length > 0) {
+ // if there's records linked to this payment method
+ var content = '';
+ result[pm_id].forEach(function (sub) {
+ content += '<p><a href="' + sub.url + '" title="' + sub.description + '">' + sub.name + '</a></p>';
+ });
+
+ content = $('<div>').html('<p>' + _t('This card is currently linked to the following records:') + '</p>' + content);
+ // Then we display the list of the records and ask the user if he really want to remove the payment method.
+ new Dialog(self, {
+ title: _t('Warning!'),
+ size: 'medium',
+ $content: content,
+ buttons: [
+ {text: _t('Confirm Deletion'), classes: 'btn-primary', close: true, click: tokenDelete},
+ {text: _t('Cancel'), close: true}]
+ }).open();
+ }
+ else {
+ // if there's no records linked to this payment method, then we delete it
+ tokenDelete();
+ }
+ }, function (err, event) {
+ self.displayError(
+ _t('Server Error'),
+ _t("We are not able to delete your payment method at the moment.") + err.data.message
+ );
+ });
+ },
+ /**
+ * Called when clicking on 'and more' to show more payment icon.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ onClickMorePaymentIcon: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ var $listItems = $(ev.currentTarget).parents('ul').children('li');
+ var $moreItem = $(ev.currentTarget).parents('li');
+ $listItems.removeClass('d-none');
+ $moreItem.addClass('d-none');
+ },
+ /**
+ * Called when clicking on a radio button.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ radioClickEvent: function (ev) {
+ // radio button checked when we click on entire zone(body) of the payment acquirer
+ $(ev.currentTarget).find('input[type="radio"]').prop("checked", true);
+ this.updateNewPaymentDisplayStatus();
+ },
+});
+return publicWidget.registry.PaymentForm;
+});
diff --git a/addons/payment/static/src/js/payment_portal.js b/addons/payment/static/src/js/payment_portal.js
new file mode 100644
index 00000000..82f8647f
--- /dev/null
+++ b/addons/payment/static/src/js/payment_portal.js
@@ -0,0 +1,65 @@
+$(function () {
+
+ $('input#cc_number').payment('formatCardNumber');
+ $('input#cc_cvc').payment('formatCardCVC');
+ $('input#cc_expiry').payment('formatCardExpiry')
+
+ $('input#cc_number').on('focusout', function (e) {
+ var valid_value = $.payment.validateCardNumber(this.value);
+ var card_type = $.payment.cardType(this.value);
+ if (card_type) {
+ $(this).parent('.form-group').children('.card_placeholder').removeClass().addClass('card_placeholder ' + card_type);
+ $(this).parent('.form-group').children('input[name="cc_brand"]').val(card_type)
+ }
+ else {
+ $(this).parent('.form-group').children('.card_placeholder').removeClass().addClass('card_placeholder');
+ }
+ if (valid_value) {
+ $(this).parent('.form-group').addClass('o_has_success').find('.form-control, .custom-select').addClass('is-valid');
+ $(this).parent('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
+ $(this).siblings('.o_invalid_field').remove();
+ }
+ else {
+ $(this).parent('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
+ $(this).parent('.form-group').removeClass('o_has_success').find('.form-control, .custom-select').removeClass('is-valid');
+ }
+ });
+
+ $('input#cc_cvc').on('focusout', function (e) {
+ var cc_nbr = $(this).parents('.oe_cc').find('#cc_number').val();
+ var card_type = $.payment.cardType(cc_nbr);
+ var valid_value = $.payment.validateCardCVC(this.value, card_type);
+ if (valid_value) {
+ $(this).parent('.form-group').addClass('o_has_success').find('.form-control, .custom-select').addClass('is-valid');
+ $(this).parent('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
+ $(this).siblings('.o_invalid_field').remove();
+ }
+ else {
+ $(this).parent('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
+ $(this).parent('.form-group').removeClass('o_has_success').find('.form-control, .custom-select').removeClass('is-valid');
+ }
+ });
+
+ $('input#cc_expiry').on('focusout', function (e) {
+ var expiry_value = $.payment.cardExpiryVal(this.value);
+ var month = expiry_value.month || '';
+ var year = expiry_value.year || '';
+ var valid_value = $.payment.validateCardExpiry(month, year);
+ if (valid_value) {
+ $(this).parent('.form-group').addClass('o_has_success').find('.form-control, .custom-select').addClass('is-valid');
+ $(this).parent('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
+ $(this).siblings('.o_invalid_field').remove();
+ }
+ else {
+ $(this).parent('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
+ $(this).parent('.form-group').removeClass('o_has_success').find('.form-control, .custom-select').removeClass('is-valid');
+ }
+ });
+
+ $('select[name="pm_acquirer_id"]').on('change', function() {
+ var acquirer_id = $(this).val();
+ $('.acquirer').addClass('d-none');
+ $('.acquirer[data-acquirer-id="'+acquirer_id+'"]').removeClass('d-none');
+ });
+
+});
diff --git a/addons/payment/static/src/js/payment_processing.js b/addons/payment/static/src/js/payment_processing.js
new file mode 100644
index 00000000..5ed13cf1
--- /dev/null
+++ b/addons/payment/static/src/js/payment_processing.js
@@ -0,0 +1,121 @@
+odoo.define('payment.processing', function (require) {
+ 'use strict';
+
+ var publicWidget = require('web.public.widget');
+ var ajax = require('web.ajax');
+ var core = require('web.core');
+
+ var _t = core._t;
+
+ $.blockUI.defaults.css.border = '0';
+ $.blockUI.defaults.css["background-color"] = '';
+ $.blockUI.defaults.overlayCSS["opacity"] = '0.9';
+
+ publicWidget.registry.PaymentProcessing = publicWidget.Widget.extend({
+ selector: '.o_payment_processing',
+ xmlDependencies: ['/payment/static/src/xml/payment_processing.xml'],
+
+ _pollCount: 0,
+
+ start: function() {
+ this.displayLoading();
+ this.poll();
+ return this._super.apply(this, arguments);
+ },
+ /* Methods */
+ startPolling: function () {
+ var timeout = 3000;
+ //
+ if(this._pollCount >= 10 && this._pollCount < 20) {
+ timeout = 10000;
+ }
+ else if(this._pollCount >= 20) {
+ timeout = 30000;
+ }
+ //
+ setTimeout(this.poll.bind(this), timeout);
+ this._pollCount ++;
+ },
+ poll: function () {
+ var self = this;
+ ajax.jsonRpc('/payment/process/poll', 'call', {}).then(function(data) {
+ if(data.success === true) {
+ self.processPolledData(data.transactions);
+ }
+ else {
+ switch(data.error) {
+ case "tx_process_retry":
+ break;
+ case "no_tx_found":
+ self.displayContent("payment.no_tx_found", {});
+ break;
+ default: // if an exception is raised
+ self.displayContent("payment.exception", {exception_msg: data.error});
+ break;
+ }
+ }
+ self.startPolling();
+
+ }).guardedCatch(function() {
+ self.displayContent("payment.rpc_error", {});
+ self.startPolling();
+ });
+ },
+ processPolledData: function (transactions) {
+ var render_values = {
+ 'tx_draft': [],
+ 'tx_pending': [],
+ 'tx_authorized': [],
+ 'tx_done': [],
+ 'tx_cancel': [],
+ 'tx_error': [],
+ };
+
+ if (transactions.length > 0 && ['transfer', 'sepa_direct_debit'].indexOf(transactions[0].acquirer_provider) >= 0) {
+ window.location = transactions[0].return_url;
+ return;
+ }
+
+ // group the transaction according to their state
+ transactions.forEach(function (tx) {
+ var key = 'tx_' + tx.state;
+ if(key in render_values) {
+ render_values[key].push(tx);
+ }
+ });
+
+ function countTxInState(states) {
+ var nbTx = 0;
+ for (var prop in render_values) {
+ if (states.indexOf(prop) > -1 && render_values.hasOwnProperty(prop)) {
+ nbTx += render_values[prop].length;
+ }
+ }
+ return nbTx;
+ }
+ // if there's only one tx to manage
+ if(countTxInState(['tx_done', 'tx_error', 'tx_pending', 'tx_authorized']) === 1) {
+ var tx = render_values['tx_done'][0] || render_values['tx_authorized'][0] || render_values['tx_error'][0];
+ if (tx) {
+ window.location = tx.return_url;
+ return;
+ }
+ }
+
+ this.displayContent("payment.display_tx_list", render_values);
+ },
+ displayContent: function (xmlid, render_values) {
+ var html = core.qweb.render(xmlid, render_values);
+ $.unblockUI();
+ this.$el.find('.o_payment_processing_content').html(html);
+ },
+ displayLoading: function () {
+ var msg = _t("We are processing your payment, please wait ...");
+ $.blockUI({
+ 'message': '<h2 class="text-white"><img src="/web/static/src/img/spin.png" class="fa-pulse"/>' +
+ ' <br />' + msg +
+ '</h2>'
+ });
+ },
+ });
+});
diff --git a/addons/payment/static/src/scss/payment_acquirer.scss b/addons/payment/static/src/scss/payment_acquirer.scss
new file mode 100644
index 00000000..e73582ec
--- /dev/null
+++ b/addons/payment/static/src/scss/payment_acquirer.scss
@@ -0,0 +1,62 @@
+.o_kanban_view.o_kanban_payment_acquirer {
+ &.o_kanban_ungrouped {
+ .o_kanban_record {
+ width: 500px;
+
+ .o_kanban_image {
+ float: right;
+
+ + div {
+ padding-left: 0;
+ padding-right: $o-kanban-image-width + $o-kanban-inside-hgutter;
+ }
+ }
+
+ .o_payment_acquirer_desc {
+ margin-bottom: 28px;
+
+ i.fa {
+ margin-right: 5px;
+
+ &.fa-check {
+ color: green;
+ }
+ }
+ }
+
+ .o_payment_acquirer_bottom {
+ > button {
+ position: absolute;
+ bottom: 8px;
+ right: 8px;
+ }
+ > .label {
+ position: absolute;
+ bottom: 8px;
+ left: 8px;
+ }
+ }
+ }
+ }
+}
+
+.o_form_view {
+ .o_payment_acquirer_desc {
+ margin-top: 10px;
+ ul {
+ list-style-type: none;
+ padding: 0;
+
+ i.fa {
+ margin-right: 5px;
+
+ &.fa-check {
+ color: green;
+ }
+ }
+ }
+ }
+ .o_warning_text {
+ color: #f0ad4e;
+ }
+}
diff --git a/addons/payment/static/src/scss/payment_form.scss b/addons/payment/static/src/scss/payment_form.scss
new file mode 100644
index 00000000..de1e0102
--- /dev/null
+++ b/addons/payment/static/src/scss/payment_form.scss
@@ -0,0 +1,56 @@
+.o_payment_form {
+ label > input[type="radio"], input[type="checkbox"]{
+ vertical-align: middle;
+ margin-right: 5px;
+ }
+
+ .payment_option_name {
+ font-size: 14px;
+ font-weight: normal !important;
+ font-family: Helvetica Neue, sans-serif;
+ line-height: 1.3em;
+ color: #4d4d4d;
+ }
+
+ label {
+ font-weight: normal;
+ margin-top: 5px;
+ }
+
+ .card-body:first-child {
+ border-top: 0px;
+ }
+
+ .card {
+ border-radius: 10px;
+ }
+
+ .card-footer:last-child {
+ border-bottom-right-radius: 10px !important;
+ border-bottom-left-radius: 10px !important;
+ }
+
+ .card-body {
+ border-top: 1px solid #ddd;
+ padding: 1.14em !important;
+ &.o_payment_acquirer_select:hover {
+ cursor: pointer;
+ }
+ }
+
+ .payment_icon_list {
+ position: relative;
+ li {
+ padding-left: 5px !important;
+ padding-right: 0px !important;
+ }
+
+ .more_option {
+ @include o-position-absolute($right: 10px);
+ font-size:10px;
+ }
+
+ margin-top: 0px !important;
+ margin-bottom: -5px !important;
+ }
+}
diff --git a/addons/payment/static/src/scss/portal_payment.scss b/addons/payment/static/src/scss/portal_payment.scss
new file mode 100644
index 00000000..4a28f596
--- /dev/null
+++ b/addons/payment/static/src/scss/portal_payment.scss
@@ -0,0 +1,61 @@
+input#cc_number {
+ background-repeat: no-repeat;
+ background-position: center right calc(2.7em);
+}
+
+div.card_placeholder {
+ background-image: url("/website_payment/static/src/img/placeholder.png");
+ background-repeat: no-repeat;
+ width: 32px;
+ height: 20px;
+ position: absolute;
+ top: 8px;
+ right: 20px;
+ -webkit-transition: 0.4s cubic-bezier(0.455,0.03,0.515,0.955);
+ transition: 0.4s cubic-bezier(0.455,0.03,0.515,0.955);
+ pointer-events: none;
+}
+
+/* if s2s form not in bootstrap_formatting */
+div.o_card_brand_detail {
+ position: relative;
+
+ div.card_placeholder {
+ right: 5px;
+ }
+}
+
+div.amex {
+ background-image: url("/website_payment/static/src/img/amex.png");
+ background-repeat: no-repeat;
+}
+
+div.diners {
+ background-image: url("/website_payment/static/src/img/diners.png");
+ background-repeat: no-repeat;
+}
+
+div.discover {
+ background-image: url("/website_payment/static/src/img/discover.png");
+ background-repeat: no-repeat;
+}
+
+div.jcb {
+ background-image: url("/website_payment/static/src/img/jcb.png");
+ background-repeat: no-repeat;
+}
+
+div.mastercard {
+ background-image: url("/website_payment/static/src/img/mastercard.png");
+ background-repeat: no-repeat;
+}
+
+div.visa {
+ background-image: url("/website_payment/static/src/img/visa.png");
+ background-repeat: no-repeat;
+}
+
+ul.payment_method_list img.rounded {
+ max-width: 100px;
+ max-height: 40px;
+}
diff --git a/addons/payment/static/src/xml/payment_processing.xml b/addons/payment/static/src/xml/payment_processing.xml
new file mode 100644
index 00000000..86623047
--- /dev/null
+++ b/addons/payment/static/src/xml/payment_processing.xml
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="payment" xml:space="preserve">
+ <!-- The templates here as rendered by 'payment_processing.js', you can also take
+ a look at payment_templates.xml (xmlid: payment_process_page) for more infos-->
+ <t t-name="payment.display_tx_list">
+ <div>
+ <!-- Error transactions -->
+ <div t-if="tx_error.length > 0">
+ <h1>Payments failed</h1>
+ <ul class="list-group">
+ <t t-foreach="tx_error" t-as="tx">
+ <li class="list-group-item">
+ <h4 class="list-group-item-heading mb5">
+ <t t-esc="tx['reference']"/>
+ <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
+ </h4>
+ <small class="list-group-item-text">
+ An error occured during the processing of this payment.<br/>
+ <strong>Reason:</strong> <t t-esc="tx['state_message']"/>
+ </small>
+ </li>
+ </t>
+ </ul>
+ </div>
+ <div t-if="tx_done.length > 0 || tx_authorized.length > 0 || tx_pending.length > 0">
+ <h1>Payments received</h1>
+ <div class="list-group">
+ <!-- Done transactions -->
+ <t t-foreach="tx_done" t-as="tx">
+ <a t-att-href="tx['return_url']" class="list-group-item">
+ <h4 class="list-group-item-heading mb5">
+ <t t-esc="tx['reference']"/>
+ <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
+ </h4>
+ <small class="list-group-item-text">
+ <t t-if="!tx['is_processed']">
+ Your order is being processed, please wait ... <i class="fa fa-cog fa-spin"/>
+ </t>
+ <t t-else="">
+ Your order has been processed.<br/>
+ Click here to be redirected to the confirmation page.
+ </t>
+ </small>
+ </a>
+ </t>
+ <!-- Pending transactions -->
+ <t t-foreach="tx_pending" t-as="tx">
+ <a t-att-href="tx['return_url']" class="list-group-item">
+ <h4 class="list-group-item-heading mb5">
+ <t t-esc="tx['reference']"/>
+ <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
+ </h4>
+ <small class="list-group-item-text">
+ <t t-if="tx['message_to_display']">
+ <t t-raw="tx['message_to_display']"/>
+ </t>
+ <t t-else="">
+ Your payment is in pending state.<br/>
+ You will be notified when the payment is fully confirmed.<br/>
+ You can click here to be redirected to the confirmation page.
+ </t>
+ </small>
+ </a>
+ </t>
+ <!-- Authorized transactions -->
+ <t t-foreach="tx_authorized" t-as="tx">
+ <li class="list-group-item">
+ <h4 class="list-group-item-heading mb5">
+ <t t-esc="tx['reference']"/>
+ <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
+ </h4>
+ <small class="list-group-item-text">
+ <t t-if="tx['message_to_display']">
+ <t t-raw="tx['message_to_display']"/>
+ </t>
+ <t t-else="">
+ Your payment has been received but need to be confirmed manually.<br/>
+ You will be notified when the payment is confirmed.
+ </t>
+ </small>
+ </li>
+ </t>
+ </div>
+ </div>
+ <!-- Draft transactions -->
+ <div t-if="tx_draft.length > 0">
+ <h1>Waiting for payment</h1>
+ <ul class="list-group">
+ <t t-foreach="tx_draft" t-as="tx">
+ <li class="list-group-item">
+ <h4 class="list-group-item-heading mb5">
+ <t t-esc="tx['reference']"/>
+ <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
+ </h4>
+ <small class="list-group-item-text">
+ <t t-if="tx['message_to_display']">
+ <t t-raw="tx['message_to_display']"/>
+ </t>
+ <t t-else="">
+ We are waiting for the payment acquirer to confirm the payment.
+ </t>
+ </small>
+ </li>
+ </t>
+ </ul>
+ </div>
+ <!-- Cancel transactions -->
+ <div t-if="tx_cancel.length > 0">
+ <h1>Cancelled payments</h1>
+ <ul class="list-group">
+ <t t-foreach="tx_cancel" t-as="tx">
+ <li class="list-group-item">
+ <h4 class="list-group-item-heading mb5">
+ <t t-esc="tx['reference']"/>
+ <span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
+ </h4>
+ <small class="list-group-item-text">
+ This transaction has been cancelled.<br/>
+ No payment has been processed.
+ </small>
+ </li>
+ </t>
+ </ul>
+ </div>
+ </div>
+ </t>
+
+ <t t-name="payment.no_tx_found">
+ <div class="text-center">
+ <p>We are not able to find your payment, but don't worry.</p>
+ <p>You should receive an email confirming your payment in a few minutes.</p>
+ <p>If the payment hasn't been confirmed you can contact us.</p>
+ </div>
+ </t>
+
+ <t t-name="payment.rpc_error">
+ <div class="text-center">
+ <p><strong>Server error:</strong> Unable to contact the Odoo server.</p>
+ <p>Please wait ... <i class="fa fa-refresh fa-spin"></i></p>
+ </div>
+ </t>
+
+ <t t-name="payment.exception">
+ <div class="text-center">
+ <h2>Internal server error</h2>
+ <pre><t t-esc="exception_msg"/></pre>
+ </div>
+ </t>
+
+</templates>