[IMP] payment, *: replace providers with payment methods in payment forms

Before this commit, the payment providers (e.g., Stripe, Adyen...)
available for payment were displayed on the payment forms. The customer
had to select one to process their payment. After that, the customer had
to select their preferred payment method (e.g., Credit Card,
Bancontact...) from a list of payment methods supported by the selected
provider over which the website administrator had close to no control.
This was making the payment forms confusing because the payment methods
were displayed sometimes more than once, if at all, in a non-controlled
order, and behind the selection of a payment provider that customers
should not have to deal with.

As the payment method was selected in an iframe or directly on the
provider's website, the information on the selection payment method was
not available in Odoo. This posed many problems, among which were the
impossibility of assessing whether a specific feature (e.g.,
tokenization, refunds, manual capture...) was available, not being able
to easily identify payment tokens through the payment method logo,
listing available payment methods on the website, sorting and
fine-grained configuration of the available payment method, subpar
payment method-specific display on the payment form (e.g., PayPal that
requires displaying a "Pay with PayPal" button), etc.

In this commit, the payment providers are thus replaced by the payment
methods on the payment forms. All contextually available (depending on
the country, currency, requested feature...) payment methods are
displayed one after the other on a single-level list and in the order
configured by the website administrator. Each payment method is
"powered by" (i.e., linked) to a single payment provider: the first one,
by model order, to support it. This allows, for example, offering the
PayPal payment method through Mollie, which charges low processing fees,
while also offering Klarna through Stripe, which supports more payment
methods but charges higher processing fees.

While doing so, the two different payment forms, "Checkout" and
"Manage", are also merged together in a new, configurable case-by-case,
payment form that is entirely redesigned to offer a better user
experience.

After payment, the information on the selected payment method is saved
on the transaction and eventual payment record and updated with the
information received from the provider.

task-2882677

closes odoo/odoo#120446

Related: odoo/upgrade#5103
Related: odoo/documentation#5717
Related: odoo/enterprise#40666
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Co-authored-by: Anita (anko) <anko@odoo.com>
Co-authored-by: Brieuc-brd <brd@odoo.com>
Co-authored-by: Valeriya (vchu) <vchu@odoo.com>
This commit is contained in:
Antoine Vandevenne (anv)
2023-09-22 13:32:53 +00:00
committed by Valeriya(vchu)
co-authored by Anita Brieuc-brd Valeriya
parent ec1d70617f
commit 7e012dd544
337 changed files with 8176 additions and 3678 deletions
+7
View File
@@ -1,5 +1,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Mapping of payment method codes to Authorize codes.
PAYMENT_METHODS_MAPPING = {
'amex': 'americanexpress',
'diners': 'dinersclub',
'card': 'creditcard'
}
# Mapping of payment status on Authorize side to transaction statuses.
# See https://developer.authorize.net/api/reference/index.html#transaction-reporting-get-transaction-details.
TRANSACTION_STATUS_MAPPING = {
@@ -14,25 +14,6 @@ _logger = logging.getLogger(__name__)
class AuthorizeController(http.Controller):
@http.route('/payment/authorize/get_provider_info', type='json', auth='public')
def authorize_get_provider_info(self, provider_id):
""" Return public information on the provider.
:param int provider_id: The provider handling the transaction, as a `payment.provider` id
:return: Information on the provider, namely: the state, payment method type, login ID, and
public client key
:rtype: dict
"""
provider_sudo = request.env['payment.provider'].sudo().browse(provider_id).exists()
return {
'state': provider_sudo.state,
'payment_method_type': provider_sudo.authorize_payment_method_type,
# The public API key solely used to identify the seller account with Authorize.Net
'login_id': provider_sudo.authorize_login,
# The public client key solely used to identify requests from the Accept.js suite
'client_key': provider_sudo.authorize_client_key,
}
@http.route('/payment/authorize/payment', type='json', auth='public')
def authorize_payment(self, reference, partner_id, access_token, opaque_data):
""" Make a payment request and handle the response.
@@ -37,7 +37,6 @@ class AuthorizeAPI:
self.state = provider.state
self.name = provider.authorize_login
self.transaction_key = provider.authorize_transaction_key
self.payment_method_type = provider.authorize_payment_method_type
def _make_request(self, operation, data=None):
request = {
@@ -80,10 +79,12 @@ class AuthorizeAPI:
'x_response_reason_text': response.get('err_msg')
}
else:
tx_response = response.get('transactionResponse', {})
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_response_code': tx_response.get('responseCode'),
'x_trans_id': tx_response.get('transId'),
'x_type': operation,
'payment_method_code': tx_response.get('accountType'),
}
# Customer profiles
@@ -137,7 +138,7 @@ class AuthorizeAPI:
})
payment = response.get('paymentProfile', {}).get('payment', {})
if self.payment_method_type == 'credit_card':
if 'creditCard' in payment:
# Authorize.net pads the card and account numbers with X's.
res['payment_details'] = payment.get('creditCard', {}).get('cardNumber')[-4:]
else:
@@ -1,13 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
import pprint
from odoo import _, api, fields, models
from odoo.fields import Command
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Command
from .authorize_request import AuthorizeAPI
from odoo.addons.payment_authorize.models.authorize_request import AuthorizeAPI
_logger = logging.getLogger(__name__)
@@ -27,26 +28,9 @@ class PaymentProvider(models.Model):
authorize_client_key = fields.Char(
string="API Client Key",
help="The public client key. To generate directly from Odoo or from Authorize.Net backend.")
authorize_payment_method_type = fields.Selection(
string="Allow Payments From",
help="Determines with what payment method the customer can pay.",
selection=[('credit_card', "Credit Card"), ('bank_account', "Bank Account (USA Only)")],
default='credit_card',
required_if_provider='authorize',
)
# === CONSTRAINT METHODS ===#
@api.constrains('authorize_payment_method_type')
def _check_payment_method_type(self):
for provider in self.filtered(lambda p: p.code == "authorize"):
if self.env['payment.token'].search([('provider_id', '=', provider.id)], limit=1):
raise ValidationError(_(
"There are active tokens linked to this provider. To change the payment method "
"type, please disable the provider and duplicate it. Then, change the payment "
"method type on the duplicated provider."
))
# Authorize.Net supports only one currency: "One gateway account is required for each currency"
# See https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957
@api.constrains('available_currency_ids', 'state')
@@ -68,24 +52,6 @@ class PaymentProvider(models.Model):
'support_tokenization': True,
})
# === ONCHANGE METHODS ===#
@api.onchange('authorize_payment_method_type')
def _onchange_authorize_payment_method_type(self):
if self.authorize_payment_method_type == 'bank_account':
self.display_as = _("Bank (powered by Authorize)")
self.payment_method_ids = [Command.clear()]
else:
self.display_as = _("Credit Card (powered by Authorize)")
self.payment_method_ids = [Command.set([self.env.ref(pm_xml_id).id for pm_xml_id in (
'payment.payment_method_maestro',
'payment.payment_method_mastercard',
'payment.payment_method_discover',
'payment.payment_method_diners_club_intl',
'payment.payment_method_jcb',
'payment.payment_method_visa',
)])]
# === ACTION METHODS ===#
def action_update_merchant_details(self):
@@ -138,3 +104,20 @@ class PaymentProvider(models.Model):
return res
return self.available_currency_ids[0]
def _authorize_get_inline_form_values(self):
""" Return a serialized JSON of the required values to render the inline form.
Note: `self.ensure_one()`
:return: The JSON serial of the required values to render the inline form.
:rtype: str
"""
self.ensure_one()
inline_form_values = {
'state': self.state,
'login_id': self.authorize_login,
'client_key': self.authorize_client_key,
}
return json.dumps(inline_form_values)
@@ -17,8 +17,3 @@ class PaymentToken(models.Model):
authorize_profile = fields.Char(
string="Authorize.Net Profile ID",
help="The unique reference for the partner/token combination in the Authorize.net backend.")
authorize_payment_method_type = fields.Selection(
string="Authorize.Net Payment Type",
help="The type of payment method this token is linked to.",
selection=[("credit_card", "Credit Card"), ("bank_account", "Bank Account (USA Only)")],
)
@@ -8,7 +8,7 @@ from odoo.exceptions import UserError, ValidationError
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_authorize.models.authorize_request import AuthorizeAPI
from odoo.addons.payment_authorize.const import TRANSACTION_STATUS_MAPPING
from odoo.addons.payment_authorize.const import PAYMENT_METHODS_MAPPING, TRANSACTION_STATUS_MAPPING
_logger = logging.getLogger(__name__)
@@ -211,7 +211,17 @@ class PaymentTransaction(models.Model):
response_content = notification_data.get('response')
# Update the provider reference.
self.provider_reference = response_content.get('x_trans_id')
# Update the payment method.
payment_method_code = response_content.get('payment_method_code', '').lower()
payment_method = self.env['payment.method']._get_from_code(
payment_method_code, mapping=PAYMENT_METHODS_MAPPING
)
self.payment_method_id = payment_method or self.payment_method_id
# Update the payment state.
status_code = response_content.get('x_response_code', '3')
if status_code == '1': # Approved
status_type = response_content.get('x_type').lower()
@@ -277,12 +287,11 @@ class PaymentTransaction(models.Model):
if cust_profile:
token = self.env['payment.token'].create({
'provider_id': self.provider_id.id,
'payment_method_id': self.payment_method_id.id,
'payment_details': cust_profile.get('payment_details'),
'partner_id': self.partner_id.id,
'provider_ref': cust_profile.get('payment_profile_id'),
'authorize_profile': cust_profile.get('profile_id'),
'authorize_payment_method_type': self.provider_id.authorize_payment_method_type,
'verified': True,
})
self.write({
'token_id': token.id,
@@ -4,34 +4,182 @@
import { _t } from '@web/core/l10n/translation';
import { loadJS } from '@web/core/assets';
import checkoutForm from '@payment/js/checkout_form';
import manageForm from '@payment/js/manage_form';
import paymentForm from '@payment/js/payment_form';
const authorizeMixin = {
paymentForm.include({
authorizeData: undefined,
// #=== DOM MANIPULATION ===#
/**
* Prepare the inline form of Authorize.net for direct payment.
*
* @private
* @param {number} providerId - The id of the selected payment option's provider.
* @param {string} providerCode - The code of the selected payment option's provider.
* @param {number} paymentOptionId - The id of the selected payment option.
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
* @param {string} flow - The online payment flow of the selected payment option.
* @return {void}
*/
async _prepareInlineForm(providerId, providerCode, paymentOptionId, paymentMethodCode, flow) {
if (providerCode !== 'authorize') {
this._super(...arguments);
return;
}
// Check if the inline form values were already extracted.
this.authorizeData ??= {}; // Store the form data of each instantiated payment method.
if (flow === 'token') {
return; // Don't show the form for tokens.
} else if (this.authorizeData[paymentOptionId]) {
this._setPaymentFlow('direct'); // Overwrite the flow even if no re-instantiation.
loadJS(this.authorizeData[paymentOptionId]['acceptJSUrl']); // Reload the SDK.
return; // Don't re-extract the data if already done for this payment method.
}
// Overwrite the flow of the selected payment method.
this._setPaymentFlow('direct');
// Extract and deserialize the inline form values.
const radio = document.querySelector('input[name="o_payment_radio"]:checked');
const inlineForm = this._getInlineForm(radio);
const authorizeForm = inlineForm.querySelector('[name="o_authorize_form"]');
this.authorizeData[paymentOptionId] = JSON.parse(authorizeForm.dataset['inlineFormValues']);
let acceptJSUrl = 'https://js.authorize.net/v1/Accept.js';
if (this.authorizeData[paymentOptionId].state !== 'enabled') {
acceptJSUrl = 'https://jstest.authorize.net/v1/Accept.js';
}
this.authorizeData[paymentOptionId].form = authorizeForm;
this.authorizeData[paymentOptionId].acceptJSUrl = acceptJSUrl;
// Load the SDK.
loadJS(acceptJSUrl);
},
// #=== PAYMENT FLOW ===#
/**
* Trigger the payment processing by submitting the data.
*
* @override method from payment.payment_form
* @private
* @param {string} providerCode - The code of the selected payment option's provider.
* @param {number} paymentOptionId - The id of the selected payment option.
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
* @param {string} flow - The payment flow of the selected payment option.
* @return {void}
*/
async _initiatePaymentFlow(providerCode, paymentOptionId, paymentMethodCode, flow) {
if (providerCode !== 'authorize' || flow === 'token') {
this._super(...arguments); // Tokens are handled by the generic flow
return;
}
const inputs = Object.values(
this._authorizeGetInlineFormInputs(paymentOptionId, paymentMethodCode)
);
if (!inputs.every(element => element.reportValidity())) {
this._enableButton(); // The submit button is disabled at this point, enable it
return;
}
await this._super(...arguments);
},
/**
* Process the direct payment flow.
*
* @override method from payment.payment_form
* @private
* @param {string} providerCode - The code of the selected payment option's provider.
* @param {number} paymentOptionId - The id of the selected payment option.
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
* @param {object} processingValues - The processing values of the transaction.
* @return {void}
*/
async _processDirectFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) {
if (providerCode !== 'authorize') {
this._super(...arguments);
return;
}
// Build the authentication and card data objects to be dispatched to Authorized.Net
const secureData = {
authData: {
apiLoginID: this.authorizeData[paymentOptionId]['login_id'],
clientKey: this.authorizeData[paymentOptionId]['client_key'],
},
...this._authorizeGetPaymentDetails(paymentOptionId, paymentMethodCode),
};
// Dispatch secure data to Authorize.Net to get a payment nonce in return
Accept.dispatchData(
secureData, response => this._authorizeHandleResponse(response, processingValues)
);
},
/**
* Handle the response from Authorize.Net and initiate the payment.
*
* @private
* @param {object} response - The payment nonce returned by Authorized.Net
* @param {object} processingValues - The processing values of the transaction.
* @return {void}
*/
_authorizeHandleResponse(response, processingValues) {
if (response.messages.resultCode === 'Error') {
let error = '';
response.messages.message.forEach(msg => error += `${msg.code}: ${msg.text}\n`);
this._displayErrorDialog(_t("Payment processing failed"), error);
this._enableButton();
return;
}
// Initiate the payment
this._rpc({
route: '/payment/authorize/payment',
params: {
'reference': processingValues.reference,
'partner_id': processingValues.partner_id,
'opaque_data': response.opaqueData,
'access_token': processingValues.access_token,
}
}).then(() => {
window.location = '/payment/status';
}).guardedCatch((error) => {
error.event.preventDefault();
this._displayErrorDialog(_t("Payment processing failed"), error.message.data.message);
this._enableButton();
});
},
// #=== GETTERS ===#
/**
* Return all relevant inline form inputs based on the payment method type of the provider.
*
* @private
* @param {number} providerId - The id of the selected provider
* @param {number} paymentOptionId - The id of the selected payment option.
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
* @return {Object} - An object mapping the name of inline form inputs to their DOM element
*/
_getInlineFormInputs: function (providerId) {
if (this.authorizeInfo['payment_method_type'] === 'credit_card') {
_authorizeGetInlineFormInputs(paymentOptionId, paymentMethodCode) {
const form = this.authorizeData[paymentOptionId]['form'];
if (paymentMethodCode === 'card') {
return {
card: this.el.querySelector(`#o_authorize_card_${providerId}`),
month: this.el.querySelector(`#o_authorize_month_${providerId}`),
year: this.el.querySelector(`#o_authorize_year_${providerId}`),
code: this.el.querySelector(`#o_authorize_code_${providerId}`),
card: form.querySelector('#o_authorize_card'),
month: form.querySelector('#o_authorize_month'),
year: form.querySelector('#o_authorize_year'),
code: form.querySelector('#o_authorize_code'),
};
} else {
return {
accountName: this.el.querySelector(`#o_authorize_account_name_${providerId}`),
accountNumber: this.el.querySelector(
`#o_authorize_account_number_${providerId}`
),
abaNumber: this.el.querySelector(`#o_authorize_aba_number_${providerId}`),
accountType: this.el.querySelector(`#o_authorize_account_type_${providerId}`),
accountName: form.querySelector('#o_authorize_account_name'),
accountNumber: form.querySelector('#o_authorize_account_number'),
abaNumber: form.querySelector('#o_authorize_aba_number'),
accountType: form.querySelector('#o_authorize_account_type'),
};
}
},
@@ -40,12 +188,13 @@ const authorizeMixin = {
* Return the credit card or bank data to pass to the Accept.dispatch request.
*
* @private
* @param {number} providerId - The id of the selected provider
* @param {number} paymentOptionId - The id of the selected payment option.
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
* @return {Object} - Data to pass to the Accept.dispatch request
*/
_getPaymentDetails: function (providerId) {
const inputs = this._getInlineFormInputs(providerId);
if (this.authorizeInfo['payment_method_type'] === 'credit_card') {
_authorizeGetPaymentDetails(paymentOptionId, paymentMethodCode) {
const inputs = this._authorizeGetInlineFormInputs(paymentOptionId, paymentMethodCode);
if (paymentMethodCode === 'card') {
return {
cardData: {
cardNumber: inputs.card.value.replace(/ /g, ''), // Remove all spaces
@@ -66,148 +215,4 @@ const authorizeMixin = {
}
},
/**
* Prepare the inline form of Authorize.Net for direct payment.
*
* @override method from @payment/js/payment_form_mixin
* @private
* @param {string} code - The code of the selected payment option's provider
* @param {number} paymentOptionId - The id of the selected payment option
* @param {string} flow - The online payment flow of the selected payment option
* @return {void}
*/
_prepareInlineForm: function (code, paymentOptionId, flow) {
if (code !== 'authorize') {
this._super(...arguments);
return;
}
if (flow === 'token') {
return; // Don't show the form for tokens
}
this._setPaymentFlow('direct');
let acceptJSUrl = 'https://js.authorize.net/v1/Accept.js';
this._rpc({
route: '/payment/authorize/get_provider_info',
params: {
'provider_id': paymentOptionId,
},
}).then(providerInfo => {
if (providerInfo.state !== 'enabled') {
acceptJSUrl = 'https://jstest.authorize.net/v1/Accept.js';
}
this.authorizeInfo = providerInfo;
}).then(() => {
loadJS(acceptJSUrl);
}).guardedCatch((error) => {
error.event.preventDefault();
this._displayError(
_t("Server Error"),
_t("An error occurred when displayed this payment form."),
error.message.data.message,
);
});
},
/**
* Dispatch the secure data to Authorize.Net.
*
* @override method from @payment/js/payment_form_mixin
* @private
* @param {string} code - The code of the payment option's provider
* @param {number} paymentOptionId - The id of the payment option handling the transaction
* @param {string} flow - The online payment flow of the transaction
* @return {void}
*/
_processPayment: function (code, paymentOptionId, flow) {
if (code !== 'authorize' || flow === 'token') {
this._super(...arguments); // Tokens are handled by the generic flow
return;
}
if (!this._validateFormInputs(paymentOptionId)) {
this._enableButton(); // The submit button is disabled at this point, enable it
this.call('ui', 'unblock'); // The page is blocked at this point, unblock it
return;
}
// Build the authentication and card data objects to be dispatched to Authorized.Net
const secureData = {
authData: {
apiLoginID: this.authorizeInfo.login_id,
clientKey: this.authorizeInfo.client_key,
},
...this._getPaymentDetails(paymentOptionId),
};
// Dispatch secure data to Authorize.Net to get a payment nonce in return
Accept.dispatchData(
secureData, response => this._responseHandler(paymentOptionId, response)
);
},
/**
* Handle the response from Authorize.Net and initiate the payment.
*
* @private
* @param {number} providerId - The id of the selected provider
* @param {object} response - The payment nonce returned by Authorized.Net
* @return {void}
*/
_responseHandler: function (providerId, response) {
if (response.messages.resultCode === 'Error') {
let error = '';
response.messages.message.forEach(msg => error += `${msg.code}: ${msg.text}\n`);
this._displayError(
_t("Server Error"),
_t("We are not able to process your payment."),
error
);
return;
}
// Create the transaction and retrieve the processing values
this._rpc({
route: this.txContext.transactionRoute,
params: this._prepareTransactionRouteParams('authorize', providerId, 'direct'),
}).then(processingValues => {
// Initiate the payment
return this._rpc({
route: '/payment/authorize/payment',
params: {
'reference': processingValues.reference,
'partner_id': processingValues.partner_id,
'opaque_data': response.opaqueData,
'access_token': processingValues.access_token,
}
});
}).then(() => {
window.location = '/payment/status';
}).guardedCatch((error) => {
error.event.preventDefault();
this._displayError(
_t("Server Error"),
_t("We are not able to process your payment."),
error.message.data.message,
);
});
},
/**
* Checks that all payment inputs adhere to the DOM validation constraints.
*
* @private
* @param {number} providerId - The id of the selected provider
* @return {boolean} - Whether all elements pass the validation constraints
*/
_validateFormInputs: function (providerId) {
const inputs = Object.values(this._getInlineFormInputs(providerId));
return inputs.every(element => element.reportValidity());
},
};
checkoutForm.include(authorizeMixin);
manageForm.include(authorizeMixin);
});
@@ -1,15 +1,16 @@
/* Disable the number input arrows. */
.o_authorize_form {
label {
margin-top: 0 !important;
}
/* Chrome, Safari, Edge, Opera */
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
}
@@ -2,50 +2,55 @@
<odoo>
<template id="inline_form">
<div t-if="provider.authorize_payment_method_type == 'credit_card'" t-attf-id="o_authorize_form_{{provider_id}}" class="o_authorize_form">
<div class="mb-3">
<label t-attf-for="o_authorize_card_{{provider_id}}" class="col-form-label">Card Number</label>
<input type="text" t-attf-id="o_authorize_card_{{provider_id}}" required="" maxlength="19" class="form-control"/>
</div>
<div class="row">
<div class="col-sm-8 mb-3">
<label t-attf-for="o_authorize_month_{{provider_id}}">Expiration</label>
<div class="input-group">
<input type="number" t-attf-id="o_authorize_month_{{provider_id}}" placeholder="MM" min="1" max="12" required="" class="form-control"/>
<input type="number" t-attf-id="o_authorize_year_{{provider_id}}" placeholder="YY" min="00" max="99" required="" class="form-control"/>
<div name="o_authorize_form"
class="o_authorize_form"
t-att-data-inline-form-values="provider_sudo._authorize_get_inline_form_values()"
>
<t t-if="pm_sudo.code == 'card'">
<div class="mb-3">
<label for="o_authorize_card" class="col-form-label">Card Number</label>
<input type="text" id="o_authorize_card" required="" maxlength="19" class="form-control"/>
</div>
<div class="row">
<div class="col-sm-8 mb-3">
<label for="o_authorize_month">Expiration</label>
<div class="input-group">
<input type="number" id="o_authorize_month" placeholder="MM" min="1" max="12" required="" class="form-control"/>
<input type="number" id="o_authorize_year" placeholder="YY" min="00" max="99" required="" class="form-control"/>
</div>
</div>
<div class="col-sm-4 mb-3">
<label for="o_authorize_code">Card Code</label>
<input type="number" id="o_authorize_code" max="9999" class="form-control"/>
</div>
</div>
<div class="col-sm-4 mb-3">
<label t-attf-for="o_authorize_code_{{provider_id}}">Card Code</label>
<input type="number" t-attf-id="o_authorize_code_{{provider_id}}" max="9999" class="form-control"/>
</t>
<t t-else="" >
<div class="mb-3">
<label for="o_authorize_bank_name" class="col-form-label">Bank Name</label>
<input type="text" id="o_authorize_bank_name" required="" class="form-control"/>
</div>
</div>
</div>
<div t-else="" t-attf-id="o_authorize_form_{{provider_id}}" class="o_authorize_form">
<div class="mb-3">
<label t-attf-for="o_authorize_bank_name_{{provider_id}}" class="col-form-label">Bank Name</label>
<input type="text" t-attf-id="o_authorize_bank_name_{{provider_id}}" required="" class="form-control"/>
</div>
<div class="mb-3">
<label t-attf-for="o_authorize_account_name_{{provider_id}}" class="col-form-label">Name On Account</label>
<input type="text" t-attf-id="o_authorize_account_name_{{provider_id}}" required="" class="form-control"/>
</div>
<div class="mb-3">
<label t-attf-for="o_authorize_account_number_{{provider_id}}" class="col-form-label">Account Number</label>
<input type="text" t-attf-id="o_authorize_account_number_{{provider_id}}" required="" class="form-control"/>
</div>
<div class="mb-3">
<label t-attf-for="o_authorize_aba_number_{{provider_id}}" class="col-form-label">ABA Routing Number</label>
<input type="text" t-attf-id="o_authorize_aba_number_{{provider_id}}" required="" class="form-control"/>
</div>
<div class="mb-3">
<label t-attf-for="o_authorize_account_type_{{provider_id}}" class="col-form-label">Bank Account Type</label>
<select t-attf-id="o_authorize_account_type_{{provider_id}}" required="" class="form-select">
<option value="checking">Personal Checking</option>
<option value="savings">Personal Savings</option>
<option value="businessChecking">Business Checking</option>
</select>
</div>
<div class="mb-3">
<label for="o_authorize_account_name" class="col-form-label">Name On Account</label>
<input type="text" id="o_authorize_account_name" required="" class="form-control"/>
</div>
<div class="mb-3">
<label for="o_authorize_account_number" class="col-form-label">Account Number</label>
<input type="text" id="o_authorize_account_number" required="" class="form-control"/>
</div>
<div class="mb-3">
<label for="o_authorize_aba_number" class="col-form-label">ABA Routing Number</label>
<input type="text" id="o_authorize_aba_number" required="" class="form-control"/>
</div>
<div class="mb-3">
<label for="o_authorize_account_type" class="col-form-label">Bank Account Type</label>
<select id="o_authorize_account_type" required="" class="form-select">
<option value="checking">Personal Checking</option>
<option value="savings">Personal Savings</option>
<option value="businessChecking">Business Checking</option>
</select>
</div>
</t>
</div>
</template>
@@ -23,11 +23,6 @@
</a>
</group>
</group>
<field name="display_as" position="before">
<field name="authorize_payment_method_type"
invisible="code != 'authorize'"
required="code == 'authorize' and state != 'disabled'"/>
</field>
<div name="available_currencies" position="inside">
<button string="Set Account Currency"
type="object"
@@ -9,7 +9,6 @@
<field name="provider_ref" position="after">
<field name="provider_code" invisible="1"/>
<field name="authorize_profile" invisible="provider_code != 'authorize'"/>
<field name="authorize_payment_method_type" invisible="provider_code != 'authorize'"/>
</field>
</field>
</record>