[IMP] payment: rename of acquirer to provider

Changing the name of model payment.acquirer to payment.provider
and everything that it touches. It is technically incorrect to
use the term "acquirer" for systems that only provide a service
of payment.
After this commit the model payment.acquirer and all related to
it will be renamed to payment.provider.

Task - 2842088

closes odoo/odoo#90899

Related: odoo/upgrade#3542
Related: odoo/documentation#1981
Related: odoo/enterprise#27131
Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
This commit is contained in:
Horacio Tellez
2022-09-09 13:38:08 +02:00
parent 29b7c5e8a5
commit f7b8f07501
337 changed files with 2476 additions and 2470 deletions
+2 -2
View File
@@ -126,7 +126,7 @@ class AccountJournal(models.Model):
copy=False, copy=False,
check_company=True, check_company=True,
help="Manual: Get paid by any method outside of Odoo.\n" help="Manual: Get paid by any method outside of Odoo.\n"
"Payment Acquirers: Each payment acquirer has its own Payment Method. Request a transaction on/to a card thanks to a payment token saved by the partner when buying or subscribing online.\n" "Payment Providers: Each payment provider has its own Payment Method. Request a transaction on/to a card thanks to a payment token saved by the partner when buying or subscribing online.\n"
"Batch Deposit: Collect several customer checks at once generating and submitting a batch deposit to your bank. Module account_batch_payment is necessary.\n" "Batch Deposit: Collect several customer checks at once generating and submitting a batch deposit to your bank. Module account_batch_payment is necessary.\n"
"SEPA Direct Debit: Get paid in the SEPA zone thanks to a mandate your partner will have granted to you. Module account_sepa is necessary.\n" "SEPA Direct Debit: Get paid in the SEPA zone thanks to a mandate your partner will have granted to you. Module account_sepa is necessary.\n"
) )
@@ -901,7 +901,7 @@ class AccountJournal(models.Model):
This getter is here to allow filtering the payment method lines if needed in other modules. This getter is here to allow filtering the payment method lines if needed in other modules.
It does NOT serve as a general getter to get the lines. It does NOT serve as a general getter to get the lines.
For example, it'll be extended to filter out lines from inactive payment acquirers in the payment module. For example, it'll be extended to filter out lines from inactive payment providers in the payment module.
:param payment_type: either inbound or outbound, used to know which lines to return :param payment_type: either inbound or outbound, used to know which lines to return
:return: Either the inbound or outbound payment method lines :return: Either the inbound or outbound payment method lines
""" """
+1 -1
View File
@@ -47,7 +47,7 @@ class AccountPayment(models.Model):
compute='_compute_payment_method_line_id', compute='_compute_payment_method_line_id',
domain="[('id', 'in', available_payment_method_line_ids)]", domain="[('id', 'in', available_payment_method_line_ids)]",
help="Manual: Pay or Get paid by any method outside of Odoo.\n" help="Manual: Pay or Get paid by any method outside of Odoo.\n"
"Payment Acquirers: Each payment acquirer has its own Payment Method. Request a transaction on/to a card thanks to a payment token saved by the partner when buying or subscribing online.\n" "Payment Providers: Each payment provider has its own Payment Method. Request a transaction on/to a card thanks to a payment token saved by the partner when buying or subscribing online.\n"
"Check: Pay bills by check and print it from Odoo.\n" "Check: Pay bills by check and print it from Odoo.\n"
"Batch Deposit: Collect several customer checks at once generating and submitting a batch deposit to your bank. Module account_batch_payment is necessary.\n" "Batch Deposit: Collect several customer checks at once generating and submitting a batch deposit to your bank. Module account_batch_payment is necessary.\n"
"SEPA Credit Transfer: Pay in the SEPA zone by submitting a SEPA Credit Transfer file to your bank. Module account_sepa is necessary.\n" "SEPA Credit Transfer: Pay in the SEPA zone by submitting a SEPA Credit Transfer file to your bank. Module account_sepa is necessary.\n"
@@ -67,7 +67,7 @@ class AccountPaymentMethod(models.Model):
""" """
Contains details about how to initialize a payment method with the code x. Contains details about how to initialize a payment method with the code x.
The contained info are: The contained info are:
mode: Either unique if we only want one of them at a single time (payment acquirers for example) mode: Either unique if we only want one of them at a single time (payment providers for example)
or multi if we want the method on each journal fitting the domain. or multi if we want the method on each journal fitting the domain.
domain: The domain defining the eligible journals. domain: The domain defining the eligible journals.
currency_id: The id of the currency necessary on the journal (or company) for it to be eligible. currency_id: The id of the currency necessary on the journal (or company) for it to be eligible.
+1 -1
View File
@@ -224,7 +224,7 @@ class ResCompany(models.Model):
# YTI FIXME: Define only one method that returns {'account': [], 'sale': [], ...} # YTI FIXME: Define only one method that returns {'account': [], 'sale': [], ...}
def get_account_invoice_onboarding_steps_states_names(self): def get_account_invoice_onboarding_steps_states_names(self):
""" Necessary to add/edit steps from other modules (payment acquirer in this case). """ """ Necessary to add/edit steps from other modules (payment provider in this case). """
return [ return [
'base_onboarding_company_state', 'base_onboarding_company_state',
'account_onboarding_invoice_layout_state', 'account_onboarding_invoice_layout_state',
@@ -88,7 +88,7 @@ class AccountPaymentRegister(models.TransientModel):
compute='_compute_payment_method_line_id', compute='_compute_payment_method_line_id',
domain="[('id', 'in', available_payment_method_line_ids)]", domain="[('id', 'in', available_payment_method_line_ids)]",
help="Manual: Pay or Get paid by any method outside of Odoo.\n" help="Manual: Pay or Get paid by any method outside of Odoo.\n"
"Payment Acquirers: Each payment acquirer has its own Payment Method. Request a transaction on/to a card thanks to a payment token saved by the partner when buying or subscribing online.\n" "Payment Providers: Each payment provider has its own Payment Method. Request a transaction on/to a card thanks to a payment token saved by the partner when buying or subscribing online.\n"
"Check: Pay bills by check and print it from Odoo.\n" "Check: Pay bills by check and print it from Odoo.\n"
"Batch Deposit: Collect several customer checks at once generating and submitting a batch deposit to your bank. Module account_batch_payment is necessary.\n" "Batch Deposit: Collect several customer checks at once generating and submitting a batch deposit to your bank. Module account_batch_payment is necessary.\n"
"SEPA Credit Transfer: Pay in the SEPA zone by submitting a SEPA Credit Transfer file to your bank. Module account_sepa is necessary.\n" "SEPA Credit Transfer: Pay in the SEPA zone by submitting a SEPA Credit Transfer file to your bank. Module account_sepa is necessary.\n"
+5 -5
View File
@@ -10,16 +10,16 @@ from odoo import api, SUPERUSER_ID
def post_init_hook(cr, registry): def post_init_hook(cr, registry):
""" Create `account.payment.method` records for the installed payment providers. """ """ Create `account.payment.method` records for the installed payment providers. """
env = api.Environment(cr, SUPERUSER_ID, {}) env = api.Environment(cr, SUPERUSER_ID, {})
PaymentAcquirer = env['payment.acquirer'] PaymentProvider = env['payment.provider']
installed_providers = PaymentAcquirer.search([('module_id.state', '=', 'installed')]) installed_providers = PaymentProvider.search([('module_id.state', '=', 'installed')])
for provider_code in set(installed_providers.mapped('provider')): for code in set(installed_providers.mapped('code')):
PaymentAcquirer._setup_payment_method(provider_code) PaymentProvider._setup_payment_method(code)
def uninstall_hook(cr, registry): def uninstall_hook(cr, registry):
""" Delete `account.payment.method` records created for the installed payment providers. """ """ Delete `account.payment.method` records created for the installed payment providers. """
env = api.Environment(cr, SUPERUSER_ID, {}) env = api.Environment(cr, SUPERUSER_ID, {})
installed_providers = env['payment.acquirer'].search([('module_id.state', '=', 'installed')]) installed_providers = env['payment.provider'].search([('module_id.state', '=', 'installed')])
env['account.payment.method'].search([ env['account.payment.method'].search([
('code', 'in', installed_providers.mapped('provider')), ('code', 'in', installed_providers.mapped('provider')),
('payment_type', '=', 'inbound'), ('payment_type', '=', 'inbound'),
+1 -1
View File
@@ -17,7 +17,7 @@
'views/account_move_views.xml', 'views/account_move_views.xml',
'views/account_journal_views.xml', 'views/account_journal_views.xml',
'views/account_payment_views.xml', 'views/account_payment_views.xml',
'views/payment_acquirer_views.xml', 'views/payment_provider_views.xml',
'views/payment_transaction_views.xml', 'views/payment_transaction_views.xml',
'wizards/account_payment_register_views.xml', 'wizards/account_payment_register_views.xml',
+9 -9
View File
@@ -22,23 +22,23 @@ class PortalAccount(portal.PortalAccount):
invoice_company = invoice.company_id or request.env.company invoice_company = invoice.company_id or request.env.company
PaymentPortal._ensure_matching_companies(partner, invoice_company) PaymentPortal._ensure_matching_companies(partner, invoice_company)
acquirers_sudo = request.env['payment.acquirer'].sudo()._get_compatible_acquirers( providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
invoice_company.id, partner.id, invoice.amount_total, currency_id=invoice.currency_id.id invoice_company.id, partner.id, invoice.amount_total, currency_id=invoice.currency_id.id
) # In sudo mode to read the fields of acquirers and partner (if not logged in) ) # In sudo mode to read the fields of providers and partner (if not logged in)
tokens = request.env['payment.token'].search( tokens = request.env['payment.token'].search(
[('acquirer_id', 'in', acquirers_sudo.ids), ('partner_id', '=', partner.id)] [('provider_id', 'in', providers_sudo.ids), ('partner_id', '=', partner.id)]
) # Tokens are cleared at the end if the user is not logged in ) # Tokens are cleared at the end if the user is not logged in
fees_by_acquirer = { fees_by_provider = {
acq_sudo: acq_sudo._compute_fees( pro_sudo: pro_sudo._compute_fees(
invoice.amount_total, invoice.currency_id, invoice.partner_id.country_id invoice.amount_total, invoice.currency_id, invoice.partner_id.country_id
) for acq_sudo in acquirers_sudo.filtered('fees_active') ) for pro_sudo in providers_sudo.filtered('fees_active')
} }
values.update({ values.update({
'acquirers': acquirers_sudo, 'providers': providers_sudo,
'tokens': tokens, 'tokens': tokens,
'fees_by_acquirer': fees_by_acquirer, 'fees_by_provider': fees_by_provider,
'show_tokenize_input': PaymentPortal._compute_show_tokenize_input_mapping( 'show_tokenize_input': PaymentPortal._compute_show_tokenize_input_mapping(
acquirers_sudo, logged_in=logged_in providers_sudo, logged_in=logged_in
), ),
'amount': invoice.amount_residual, 'amount': invoice.amount_residual,
'currency': invoice.currency_id, 'currency': invoice.currency_id,
+1 -1
View File
@@ -5,5 +5,5 @@ from . import account_move
from . import account_payment from . import account_payment
from . import account_payment_method from . import account_payment_method
from . import account_payment_method_line from . import account_payment_method_line
from . import payment_acquirer from . import payment_provider
from . import payment_transaction from . import payment_transaction
@@ -11,50 +11,52 @@ class AccountJournal(models.Model):
@api.constrains('inbound_payment_method_line_ids') @api.constrains('inbound_payment_method_line_ids')
def _check_inbound_payment_method_line_ids(self): def _check_inbound_payment_method_line_ids(self):
""" """
Check and ensure that the user do not remove a apml that is linked to an acquirer in the test or enabled state. Check and ensure that the user do not remove a apml that is linked to a provider in the test or enabled state.
""" """
if not self.company_id: if not self.company_id:
return return
self.env['account.payment.method'].flush_model(['code', 'payment_type']) self.env['account.payment.method'].flush_model(['code', 'payment_type'])
self.env['account.payment.method.line'].flush_model(['payment_method_id']) self.env['account.payment.method.line'].flush_model(['payment_method_id'])
self.env['payment.acquirer'].flush_model(['provider', 'state']) self.env['payment.provider'].flush_model(['code', 'state'])
self._cr.execute(''' self._cr.execute('''
SELECT acquirer.id SELECT provider.id
FROM payment_acquirer acquirer FROM payment_provider provider
JOIN account_payment_method apm JOIN account_payment_method apm
ON apm.code = acquirer.provider ON apm.code = provider.code
LEFT JOIN account_payment_method_line apml LEFT JOIN account_payment_method_line apml
ON apm.id = apml.payment_method_id AND apml.journal_id IS NOT NULL ON apm.id = apml.payment_method_id AND apml.journal_id IS NOT NULL
WHERE acquirer.state IN ('enabled', 'test') WHERE provider.state IN ('enabled', 'test')
AND acquirer.provider != 'custom' AND provider.code != 'custom'
AND apm.payment_type = 'inbound' AND apm.payment_type = 'inbound'
AND apml.id IS NULL AND apml.id IS NULL
AND acquirer.company_id IN %(company_ids)s AND provider.company_id IN %(company_ids)s
''', {'company_ids': tuple(self.company_id.ids)}) ''', {'company_ids': tuple(self.company_id.ids)})
ids = [r[0] for r in self._cr.fetchall()] ids = [r[0] for r in self._cr.fetchall()]
if ids: if ids:
acquirers = self.env['payment.acquirer'].sudo().browse(ids) providers = self.env['payment.provider'].sudo().browse(ids)
raise UserError(_("You can't delete a payment method that is linked to an acquirer in the enabled or test state.\n" raise UserError(
"Linked acquirer(s): %s", ', '.join(a.display_name for a in acquirers))) _("You can't delete a payment method that is linked to a provider in the enabled or test state.\n"
"Linked provider(s): %s", ', '.join(p.display_name for p in providers))
)
def _get_available_payment_method_lines(self, payment_type): def _get_available_payment_method_lines(self, payment_type):
lines = super()._get_available_payment_method_lines(payment_type) lines = super()._get_available_payment_method_lines(payment_type)
return lines.filtered(lambda l: l.payment_acquirer_state != 'disabled') return lines.filtered(lambda l: l.payment_provider_state != 'disabled')
@api.depends('outbound_payment_method_line_ids', 'inbound_payment_method_line_ids') @api.depends('outbound_payment_method_line_ids', 'inbound_payment_method_line_ids')
def _compute_available_payment_method_ids(self): def _compute_available_payment_method_ids(self):
super()._compute_available_payment_method_ids() super()._compute_available_payment_method_ids()
installed_acquirers = self.env['payment.acquirer'].sudo().search([]) installed_providers = self.env['payment.provider'].sudo().search([])
method_information = self.env['account.payment.method']._get_payment_method_information() method_information = self.env['account.payment.method']._get_payment_method_information()
pay_methods = self.env['account.payment.method'].search([('code', 'in', list(method_information.keys()))]) pay_methods = self.env['account.payment.method'].search([('code', 'in', list(method_information.keys()))])
pay_method_by_code = {x.code + x.payment_type: x for x in pay_methods} pay_method_by_code = {x.code + x.payment_type: x for x in pay_methods}
# On top of the basic filtering, filter to hide unavailable acquirers. # On top of the basic filtering, filter to hide unavailable providers.
# This avoid allowing payment method lines linked to an acquirer that has no record. # This avoid allowing payment method lines linked to a provider that has no record.
for code, vals in method_information.items(): for code, vals in method_information.items():
payment_method = pay_method_by_code.get(code + 'inbound') payment_method = pay_method_by_code.get(code + 'inbound')
@@ -64,9 +66,9 @@ class AccountJournal(models.Model):
for journal in self: for journal in self:
to_remove = [] to_remove = []
available_providers = installed_acquirers.filtered( available_providers = installed_providers.filtered(
lambda a: a.company_id == journal.company_id lambda p: p.company_id == journal.company_id
).mapped('provider') ).mapped('code')
available = payment_method.code in available_providers available = payment_method.code in available_providers
if vals['mode'] == 'unique' and not available: if vals['mode'] == 'unique' and not available:
@@ -45,13 +45,13 @@ class AccountMove(models.Model):
def payment_action_capture(self): def payment_action_capture(self):
""" Capture all transactions linked to this invoice. """ """ Capture all transactions linked to this invoice. """
payment_utils.check_rights_on_recordset(self) payment_utils.check_rights_on_recordset(self)
# In sudo mode because we need to be able to read on acquirer fields. # In sudo mode because we need to be able to read on provider fields.
self.authorized_transaction_ids.sudo().action_capture() self.authorized_transaction_ids.sudo().action_capture()
def payment_action_void(self): def payment_action_void(self):
""" Void all transactions linked to this invoice. """ """ Void all transactions linked to this invoice. """
payment_utils.check_rights_on_recordset(self) payment_utils.check_rights_on_recordset(self)
# In sudo mode because we need to be able to read on acquirer fields. # In sudo mode because we need to be able to read on provider fields.
self.authorized_transaction_ids.sudo().action_void() self.authorized_transaction_ids.sudo().action_void()
def action_view_payment_transactions(self): def action_view_payment_transactions(self):
@@ -18,7 +18,7 @@ class AccountPayment(models.Model):
string="Saved Payment Token", comodel_name='payment.token', domain="""[ string="Saved Payment Token", comodel_name='payment.token', domain="""[
('id', 'in', suitable_payment_token_ids), ('id', 'in', suitable_payment_token_ids),
]""", ]""",
help="Note that only tokens from acquirers allowing to capture the amount are available.") help="Note that only tokens from providers allowing to capture the amount are available.")
amount_available_for_refund = fields.Monetary(compute='_compute_amount_available_for_refund') amount_available_for_refund = fields.Monetary(compute='_compute_amount_available_for_refund')
# == Display purpose fields == # == Display purpose fields ==
@@ -48,7 +48,7 @@ class AccountPayment(models.Model):
def _compute_amount_available_for_refund(self): def _compute_amount_available_for_refund(self):
for payment in self: for payment in self:
tx_sudo = payment.payment_transaction_id.sudo() tx_sudo = payment.payment_transaction_id.sudo()
if tx_sudo.acquirer_id.support_refund and tx_sudo.operation != 'refund': if tx_sudo.provider_id.support_refund and tx_sudo.operation != 'refund':
# Only consider refund transactions that are confirmed by summing the amounts of # Only consider refund transactions that are confirmed by summing the amounts of
# payments linked to such refund transactions. Indeed, should a refund transaction # payments linked to such refund transactions. Indeed, should a refund transaction
# be stuck forever in a transient state (due to webhook failure, for example), the # be stuck forever in a transient state (due to webhook failure, for example), the
@@ -71,9 +71,9 @@ class AccountPayment(models.Model):
if payment.use_electronic_payment_method: if payment.use_electronic_payment_method:
payment.suitable_payment_token_ids = self.env['payment.token'].sudo().search([ payment.suitable_payment_token_ids = self.env['payment.token'].sudo().search([
('company_id', '=', payment.company_id.id), ('company_id', '=', payment.company_id.id),
('acquirer_id.capture_manually', '=', False), ('provider_id.capture_manually', '=', False),
('partner_id', 'in', related_partner_ids.ids), ('partner_id', 'in', related_partner_ids.ids),
('acquirer_id', '=', payment.payment_method_line_id.payment_acquirer_id.id), ('provider_id', '=', payment.payment_method_line_id.payment_provider_id.id),
]) ])
else: else:
payment.suitable_payment_token_ids = [Command.clear()] payment.suitable_payment_token_ids = [Command.clear()]
@@ -82,8 +82,8 @@ class AccountPayment(models.Model):
def _compute_use_electronic_payment_method(self): def _compute_use_electronic_payment_method(self):
for payment in self: for payment in self:
# Get a list of all electronic payment method codes. # Get a list of all electronic payment method codes.
# These codes are comprised of 'electronic' and the providers of each payment acquirer. # These codes are comprised of 'electronic' and the providers of each payment provider.
codes = [key for key in dict(self.env['payment.acquirer']._fields['provider']._description_selection(self.env))] codes = [key for key in dict(self.env['payment.provider']._fields['code']._description_selection(self.env))]
payment.use_electronic_payment_method = payment.payment_method_code in codes payment.use_electronic_payment_method = payment.payment_method_code in codes
def _compute_refunds_count(self): def _compute_refunds_count(self):
@@ -103,7 +103,7 @@ class AccountPayment(models.Model):
@api.onchange('partner_id', 'payment_method_line_id', 'journal_id') @api.onchange('partner_id', 'payment_method_line_id', 'journal_id')
def _onchange_set_payment_token_id(self): def _onchange_set_payment_token_id(self):
codes = [key for key in dict(self.env['payment.acquirer']._fields['provider']._description_selection(self.env))] codes = [key for key in dict(self.env['payment.provider']._fields['code']._description_selection(self.env))]
if not (self.payment_method_code in codes and self.partner_id and self.journal_id): if not (self.payment_method_code in codes and self.partner_id and self.journal_id):
self.payment_token_id = False self.payment_token_id = False
return return
@@ -117,20 +117,20 @@ class AccountPayment(models.Model):
self.payment_token_id = self.env['payment.token'].sudo().search([ self.payment_token_id = self.env['payment.token'].sudo().search([
('company_id', '=', self.company_id.id), ('company_id', '=', self.company_id.id),
('partner_id', 'in', related_partner_ids.ids), ('partner_id', 'in', related_partner_ids.ids),
('acquirer_id.capture_manually', '=', False), ('provider_id.capture_manually', '=', False),
('acquirer_id', '=', self.payment_method_line_id.payment_acquirer_id.id), ('provider_id', '=', self.payment_method_line_id.payment_provider_id.id),
], limit=1) ], limit=1)
#=== ACTION METHODS ===# #=== ACTION METHODS ===#
def action_post(self): def action_post(self):
# Post the payments "normally" if no transactions are needed. # Post the payments "normally" if no transactions are needed.
# If not, let the acquirer update the state. # If not, let the provider update the state.
payments_need_tx = self.filtered( payments_need_tx = self.filtered(
lambda p: p.payment_token_id and not p.payment_transaction_id lambda p: p.payment_token_id and not p.payment_transaction_id
) )
# creating the transaction require to access data on payment acquirers, not always accessible to users # creating the transaction require to access data on payment providers, not always accessible to users
# able to create payments # able to create payments
transactions = payments_need_tx.sudo()._create_payment_transaction() transactions = payments_need_tx.sudo()._create_payment_transaction()
@@ -203,7 +203,7 @@ class AccountPayment(models.Model):
def _prepare_payment_transaction_vals(self, **extra_create_values): def _prepare_payment_transaction_vals(self, **extra_create_values):
self.ensure_one() self.ensure_one()
return { return {
'acquirer_id': self.payment_token_id.acquirer_id.id, 'provider_id': self.payment_token_id.provider_id.id,
'reference': self.ref, 'reference': self.ref,
'amount': self.amount, 'amount': self.amount,
'currency_id': self.currency_id.id, 'currency_id': self.currency_id.id,
@@ -10,10 +10,10 @@ class AccountPaymentMethod(models.Model):
@api.model @api.model
def _get_payment_method_information(self): def _get_payment_method_information(self):
res = super()._get_payment_method_information() res = super()._get_payment_method_information()
for provider, _desc in self.env['payment.acquirer']._fields['provider'].selection: for code, _desc in self.env['payment.provider']._fields['code'].selection:
if provider in ('none', 'transfer'): if code in ('none', 'transfer'):
continue continue
res[provider] = { res[code] = {
'mode': 'unique', 'mode': 'unique',
'domain': [('type', '=', 'bank')], 'domain': [('type', '=', 'bank')],
} }
@@ -8,35 +8,35 @@ from odoo.osv import expression
class AccountPaymentMethodLine(models.Model): class AccountPaymentMethodLine(models.Model):
_inherit = "account.payment.method.line" _inherit = "account.payment.method.line"
payment_acquirer_id = fields.Many2one( payment_provider_id = fields.Many2one(
comodel_name='payment.acquirer', comodel_name='payment.provider',
compute='_compute_payment_acquirer_id', compute='_compute_payment_provider_id',
store=True store=True
) )
payment_acquirer_state = fields.Selection( payment_provider_state = fields.Selection(
related='payment_acquirer_id.state' related='payment_provider_id.state'
) )
@api.depends('payment_method_id') @api.depends('payment_method_id')
def _compute_payment_acquirer_id(self): def _compute_payment_provider_id(self):
acquirers = self.env['payment.acquirer'].sudo().search([ providers = self.env['payment.provider'].sudo().search([
('provider', 'in', self.mapped('code')), ('code', 'in', self.mapped('code')),
('company_id', 'in', self.journal_id.company_id.ids), ('company_id', 'in', self.journal_id.company_id.ids),
]) ])
# Make sure to pick the active acquirer, if any. # Make sure to pick the active provider, if any.
acquirers_map = dict() providers_map = dict()
for acquirer in acquirers: for provider in providers:
current_value = acquirers_map.get((acquirer.provider, acquirer.company_id), False) current_value = providers_map.get((provider.code, provider.company_id), False)
if current_value and current_value.state != 'disabled': if current_value and current_value.state != 'disabled':
continue continue
acquirers_map[(acquirer.provider, acquirer.company_id)] = acquirer providers_map[(provider.code, provider.company_id)] = provider
for line in self: for line in self:
code = line.payment_method_id.code code = line.payment_method_id.code
company = line.journal_id.company_id company = line.journal_id.company_id
line.payment_acquirer_id = acquirers_map.get((code, company), False) line.payment_provider_id = providers_map.get((code, company), False)
@api.model @api.model
def _get_payment_method_domain(self, code): def _get_payment_method_domain(self, code):
@@ -46,19 +46,19 @@ class AccountPaymentMethodLine(models.Model):
unique = information.get('mode') == 'unique' unique = information.get('mode') == 'unique'
if unique: if unique:
company_ids = self.env['payment.acquirer'].sudo().search([('provider', '=', code)]).mapped('company_id') company_ids = self.env['payment.provider'].sudo().search([('code', '=', code)]).mapped('company_id')
if company_ids: if company_ids:
domain = expression.AND([domain, [('company_id', 'in', company_ids.ids)]]) domain = expression.AND([domain, [('company_id', 'in', company_ids.ids)]])
return domain return domain
def action_open_acquirer_form(self): def action_open_provider_form(self):
self.ensure_one() self.ensure_one()
return { return {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'name': _('Acquirer'), 'name': _('Provider'),
'view_mode': 'form', 'view_mode': 'form',
'res_model': 'payment.acquirer', 'res_model': 'payment.provider',
'target': 'current', 'target': 'current',
'res_id': self.payment_acquirer_id.id 'res_id': self.payment_provider_id.id
} }
@@ -3,8 +3,8 @@
from odoo import api, fields, models from odoo import api, fields, models
class PaymentAcquirer(models.Model): class Paymentprovider(models.Model):
_inherit = 'payment.acquirer' _inherit = 'payment.provider'
journal_id = fields.Many2one( journal_id = fields.Many2one(
string="Payment Journal", string="Payment Journal",
@@ -17,74 +17,74 @@ class PaymentAcquirer(models.Model):
#=== COMPUTE METHODS ===# #=== COMPUTE METHODS ===#
@api.depends('provider') @api.depends('code')
def _compute_journal_id(self): def _compute_journal_id(self):
for acquirer in self: for provider in self:
payment_method = self.env['account.payment.method.line'].search([ payment_method = self.env['account.payment.method.line'].search([
('journal_id.company_id', '=', acquirer.company_id.id), ('journal_id.company_id', '=', provider.company_id.id),
('code', '=', acquirer.provider) ('code', '=', provider.code)
], limit=1) ], limit=1)
if payment_method: if payment_method:
acquirer.journal_id = payment_method.journal_id provider.journal_id = payment_method.journal_id
else: else:
acquirer.journal_id = False provider.journal_id = False
def _inverse_journal_id(self): def _inverse_journal_id(self):
for acquirer in self: for provider in self:
payment_method_line = self.env['account.payment.method.line'].search([ payment_method_line = self.env['account.payment.method.line'].search([
('journal_id.company_id', '=', acquirer.company_id.id), ('journal_id.company_id', '=', provider.company_id.id),
('code', '=', acquirer.provider), ('code', '=', provider.code),
], limit=1) ], limit=1)
if acquirer.journal_id: if provider.journal_id:
if not payment_method_line: if not payment_method_line:
default_payment_method_id = acquirer._get_default_payment_method_id( default_payment_method_id = provider._get_default_payment_method_id(
acquirer.provider provider.code
) )
existing_payment_method_line = self.env['account.payment.method.line'].search([ existing_payment_method_line = self.env['account.payment.method.line'].search([
('payment_method_id', '=', default_payment_method_id), ('payment_method_id', '=', default_payment_method_id),
('journal_id', '=', acquirer.journal_id.id), ('journal_id', '=', provider.journal_id.id),
], limit=1) ], limit=1)
if not existing_payment_method_line: if not existing_payment_method_line:
self.env['account.payment.method.line'].create({ self.env['account.payment.method.line'].create({
'payment_method_id': default_payment_method_id, 'payment_method_id': default_payment_method_id,
'journal_id': acquirer.journal_id.id, 'journal_id': provider.journal_id.id,
}) })
else: else:
payment_method_line.journal_id = acquirer.journal_id payment_method_line.journal_id = provider.journal_id
elif payment_method_line: elif payment_method_line:
payment_method_line.unlink() payment_method_line.unlink()
@api.model @api.model
def _get_default_payment_method_id(self, provider): def _get_default_payment_method_id(self, code):
provider_payment_method = self._get_provider_payment_method(provider) provider_payment_method = self._get_provider_payment_method(code)
if provider_payment_method: if provider_payment_method:
return provider_payment_method.id return provider_payment_method.id
return self.env.ref('account.account_payment_method_manual_in').id return self.env.ref('account.account_payment_method_manual_in').id
@api.model @api.model
def _get_provider_payment_method(self, provider): def _get_provider_payment_method(self, code):
return self.env['account.payment.method'].search([('code', '=', provider)], limit=1) return self.env['account.payment.method'].search([('code', '=', code)], limit=1)
#=== BUSINESS METHODS ===# #=== BUSINESS METHODS ===#
@api.model @api.model
def _setup_provider(self, provider_code): def _setup_provider(self, code):
""" Override of `payment` to create the payment method of the provider. """ """ Override of `payment` to create the payment method of the provider. """
super()._setup_provider(provider_code) super()._setup_provider(code)
self._setup_payment_method(provider_code) self._setup_payment_method(code)
@api.model @api.model
def _setup_payment_method(self, provider): def _setup_payment_method(self, code):
if provider not in ('none', 'transfer') and not self._get_provider_payment_method(provider): if code not in ('none', 'transfer') and not self._get_provider_payment_method(code):
providers_description = dict(self._fields['provider']._description_selection(self.env)) providers_description = dict(self._fields['code']._description_selection(self.env))
self.env['account.payment.method'].create({ self.env['account.payment.method'].create({
'name': providers_description[provider], 'name': providers_description[code],
'code': provider, 'code': code,
'payment_type': 'inbound', 'payment_type': 'inbound',
}) })
@api.model @api.model
def _remove_provider(self, provider): def _remove_provider(self, code):
""" Override of `payment` to delete the payment method of the provider. """ """ Override of `payment` to delete the payment method of the provider. """
super()._remove_provider(provider) super()._remove_provider(code)
self._get_provider_payment_method(provider).unlink() self._get_provider_payment_method(code).unlink()
@@ -64,7 +64,7 @@ class PaymentTransaction(models.Model):
#=== BUSINESS METHODS - PAYMENT FLOW ===# #=== BUSINESS METHODS - PAYMENT FLOW ===#
@api.model @api.model
def _compute_reference_prefix(self, provider, separator, **values): def _compute_reference_prefix(self, provider_code, separator, **values):
""" Compute the reference prefix from the transaction values. """ Compute the reference prefix from the transaction values.
If the `values` parameter has an entry with 'invoice_ids' as key and a list of (4, id, O) or If the `values` parameter has an entry with 'invoice_ids' as key and a list of (4, id, O) or
@@ -73,7 +73,7 @@ class PaymentTransaction(models.Model):
Note: This method should be called in sudo mode to give access to documents (INV, SO, ...). Note: This method should be called in sudo mode to give access to documents (INV, SO, ...).
:param str provider: The provider of the acquirer handling the transaction :param str provider_code: The code of the provider handling the transaction
:param str separator: The custom separator used to separate data references :param str separator: The custom separator used to separate data references
:param dict values: The transaction values used to compute the reference prefix. It should :param dict values: The transaction values used to compute the reference prefix. It should
have the structure {'invoice_ids': [(X2M command), ...], ...}. have the structure {'invoice_ids': [(X2M command), ...], ...}.
@@ -87,7 +87,7 @@ class PaymentTransaction(models.Model):
invoices = self.env['account.move'].browse(invoice_ids).exists() invoices = self.env['account.move'].browse(invoice_ids).exists()
if len(invoices) == len(invoice_ids): # All ids are valid if len(invoices) == len(invoice_ids): # All ids are valid
return separator.join(invoices.mapped('name')) return separator.join(invoices.mapped('name'))
return super()._compute_reference_prefix(provider, separator, **values) return super()._compute_reference_prefix(provider_code, separator, **values)
def _set_canceled(self, state_message=None): def _set_canceled(self, state_message=None):
""" Update the transactions' state to 'cancel'. """ Update the transactions' state to 'cancel'.
@@ -134,16 +134,16 @@ class PaymentTransaction(models.Model):
""" """
self.ensure_one() self.ensure_one()
payment_method_line = self.acquirer_id.journal_id.inbound_payment_method_line_ids\ payment_method_line = self.provider_id.journal_id.inbound_payment_method_line_ids\
.filtered(lambda l: l.code == self.provider) .filtered(lambda l: l.code == self.provider_code)
payment_values = { payment_values = {
'amount': abs(self.amount), # A tx may have a negative amount, but a payment must >= 0 'amount': abs(self.amount), # A tx may have a negative amount, but a payment must >= 0
'payment_type': 'inbound' if self.amount > 0 else 'outbound', 'payment_type': 'inbound' if self.amount > 0 else 'outbound',
'currency_id': self.currency_id.id, 'currency_id': self.currency_id.id,
'partner_id': self.partner_id.commercial_partner_id.id, 'partner_id': self.partner_id.commercial_partner_id.id,
'partner_type': 'customer', 'partner_type': 'customer',
'journal_id': self.acquirer_id.journal_id.id, 'journal_id': self.provider_id.journal_id.id,
'company_id': self.acquirer_id.company_id.id, 'company_id': self.provider_id.company_id.id,
'payment_method_line_id': payment_method_line.id, 'payment_method_line_id': payment_method_line.id,
'payment_token_id': self.token_id.id, 'payment_token_id': self.token_id.id,
'payment_transaction_id': self.id, 'payment_transaction_id': self.id,
@@ -15,12 +15,12 @@ odoo.define('account_payment.payment_form', require => {
* *
* @override method from payment.payment_form_mixin * @override method from payment.payment_form_mixin
* @private * @private
* @param {string} provider - The provider of the selected payment option's acquirer. * @param {string} code - The provider code of the selected payment option.
* @param {number} paymentOptionId - The id of the selected payment option. * @param {number} paymentOptionId - The id of the selected payment option.
* @param {string} flow - The online payment flow of the selected payment option. * @param {string} flow - The online payment flow of the selected payment option.
* @return {object} The extended transaction route params. * @return {object} The extended transaction route params.
*/ */
_prepareTransactionRouteParams: function (provider, paymentOptionId, flow) { _prepareTransactionRouteParams: function (code, paymentOptionId, flow) {
const transactionRouteParams = this._super(...arguments); const transactionRouteParams = this._super(...arguments);
return { return {
...transactionRouteParams, ...transactionRouteParams,
+15 -15
View File
@@ -28,7 +28,7 @@ class AccountPaymentCommon(PaymentCommon, AccountTestInvoicingCommon):
'payment_type': 'inbound' 'payment_type': 'inbound'
}) })
cls.dummy_acquirer.journal_id = cls.company_data['default_journal_bank'].id, cls.dummy_provider.journal_id = cls.company_data['default_journal_bank'].id,
cls.account = cls.company.account_journal_payment_credit_account_id cls.account = cls.company.account_journal_payment_credit_account_id
cls.invoice = cls.env['account.move'].create({ cls.invoice = cls.env['account.move'].create({
@@ -60,23 +60,23 @@ class AccountPaymentCommon(PaymentCommon, AccountTestInvoicingCommon):
#=== Utils ===# #=== Utils ===#
@classmethod @classmethod
def _prepare_acquirer(cls, provider='none', company=None, update_values=None): def _prepare_provider(cls, provider_code='none', company=None, update_values=None):
""" Override of `payment` to prepare and return the first acquirer matching the given """ Override of `payment` to prepare and return the first provider matching the given
provider and company. provider and company.
If no acquirer is found in the given company, we duplicate the one from the base company. If no provider is found in the given company, we duplicate the one from the base company.
All other acquirers belonging to the same company are disabled to avoid any interferences. All other providers belonging to the same company are disabled to avoid any interferences.
:param str provider: The provider of the acquirer to prepare. :param str provider_code: The code of the provider to prepare.
:param recordset company: The company of the acquirer to prepare, as a `res.company` record. :param recordset company: The company of the provider to prepare, as a `res.company` record.
:param dict update_values: The values used to update the acquirer. :param dict update_values: The values used to update the provider.
:return: The acquirer to prepare, if found. :return: The provider to prepare, if found.
:rtype: recordset of `payment.acquirer` :rtype: recordset of `payment.provider`
""" """
acquirer = super()._prepare_acquirer(provider, company, update_values) provider = super()._prepare_provider(provider_code, company, update_values)
if not acquirer.journal_id: if not provider.journal_id:
acquirer.journal_id = cls.env['account.journal'].search( provider.journal_id = cls.env['account.journal'].search(
[('company_id', '=', acquirer.company_id.id), ('type', '=', 'bank')], [('company_id', '=', provider.company_id.id), ('type', '=', 'bank')],
limit=1, limit=1,
) )
return acquirer return provider
@@ -11,18 +11,18 @@ from odoo.addons.account_payment.tests.common import AccountPaymentCommon
class TestAccountPayment(AccountPaymentCommon): class TestAccountPayment(AccountPaymentCommon):
def test_no_amount_available_for_refund_when_not_supported(self): def test_no_amount_available_for_refund_when_not_supported(self):
self.acquirer.support_refund = False self.provider.support_refund = False
tx = self._create_transaction('redirect', state='done') tx = self._create_transaction('redirect', state='done')
tx._reconcile_after_done() # Create the payment tx._reconcile_after_done() # Create the payment
self.assertEqual( self.assertEqual(
tx.payment_id.amount_available_for_refund, tx.payment_id.amount_available_for_refund,
0, 0,
msg="The value of `amount_available_for_refund` should be 0 when the acquirer doesn't " msg="The value of `amount_available_for_refund` should be 0 when the provider doesn't "
"support refunds." "support refunds."
) )
def test_full_amount_available_for_refund_when_not_yet_refunded(self): def test_full_amount_available_for_refund_when_not_yet_refunded(self):
self.acquirer.support_refund = 'full_only' # Should simply not be False self.provider.support_refund = 'full_only' # Should simply not be False
tx = self._create_transaction('redirect', state='done') tx = self._create_transaction('redirect', state='done')
tx._reconcile_after_done() # Create the payment tx._reconcile_after_done() # Create the payment
self.assertAlmostEqual( self.assertAlmostEqual(
@@ -34,7 +34,7 @@ class TestAccountPayment(AccountPaymentCommon):
) )
def test_full_amount_available_for_refund_when_refunds_are_pending(self): def test_full_amount_available_for_refund_when_refunds_are_pending(self):
self.acquirer.write({ self.provider.write({
'support_refund': 'full_only', # Should simply not be False 'support_refund': 'full_only', # Should simply not be False
'support_manual_capture': True, # To create transaction in the 'authorized' state 'support_manual_capture': True, # To create transaction in the 'authorized' state
}) })
@@ -58,7 +58,7 @@ class TestAccountPayment(AccountPaymentCommon):
) )
def test_no_amount_available_for_refund_when_fully_refunded(self): def test_no_amount_available_for_refund_when_fully_refunded(self):
self.acquirer.support_refund = 'full_only' # Should simply not be False self.provider.support_refund = 'full_only' # Should simply not be False
tx = self._create_transaction('redirect', state='done') tx = self._create_transaction('redirect', state='done')
tx._reconcile_after_done() # Create the payment tx._reconcile_after_done() # Create the payment
self._create_transaction( self._create_transaction(
@@ -77,7 +77,7 @@ class TestAccountPayment(AccountPaymentCommon):
) )
def test_no_full_amount_available_for_refund_when_partially_refunded(self): def test_no_full_amount_available_for_refund_when_partially_refunded(self):
self.acquirer.support_refund = 'partial' self.provider.support_refund = 'partial'
tx = self._create_transaction('redirect', state='done') tx = self._create_transaction('redirect', state='done')
tx._reconcile_after_done() # Create the payment tx._reconcile_after_done() # Create the payment
self._create_transaction( self._create_transaction(
@@ -98,7 +98,7 @@ class TestAccountPayment(AccountPaymentCommon):
) )
def test_refunds_count(self): def test_refunds_count(self):
self.acquirer.support_refund = 'full_only' # Should simply not be False self.provider.support_refund = 'full_only' # Should simply not be False
tx = self._create_transaction('redirect', state='done') tx = self._create_transaction('redirect', state='done')
tx._reconcile_after_done() # Create the payment tx._reconcile_after_done() # Create the payment
for reference_index, operation in enumerate( for reference_index, operation in enumerate(
@@ -127,7 +127,7 @@ class TestAccountPayment(AccountPaymentCommon):
'date': '2019-01-01', 'date': '2019-01-01',
'currency_id': self.currency.id, 'currency_id': self.currency.id,
'partner_id': self.partner.id, 'partner_id': self.partner.id,
'journal_id': self.acquirer.journal_id.id, 'journal_id': self.provider.journal_id.id,
'payment_method_line_id': self.inbound_payment_method_line.id, 'payment_method_line_id': self.inbound_payment_method_line.id,
}) })
payment_with_token = payment_without_token.copy() payment_with_token = payment_without_token.copy()
@@ -34,7 +34,7 @@ class TestFlows(AccountPaymentCommon, PaymentHttpCommon):
} }
route_values.update({ route_values.update({
'flow': 'direct', 'flow': 'direct',
'payment_option_id': self.acquirer.id, 'payment_option_id': self.provider.id,
'tokenization_requested': False, 'tokenization_requested': False,
}) })
with mute_logger('odoo.addons.payment.models.payment_transaction'): with mute_logger('odoo.addons.payment.models.payment_transaction'):
@@ -7,17 +7,17 @@
<field name="inherit_id" ref="account.view_account_journal_form"/> <field name="inherit_id" ref="account.view_account_journal_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='inbound_payment_method_line_ids']//field[@name='payment_account_id']" position="after"> <xpath expr="//field[@name='inbound_payment_method_line_ids']//field[@name='payment_account_id']" position="after">
<field name="payment_acquirer_id" invisible="1"/> <field name="payment_provider_id" invisible="1"/>
<field name="payment_acquirer_state" invisible="1"/> <field name="payment_provider_state" invisible="1"/>
<button name="action_open_acquirer_form" <button name="action_open_provider_form"
type="object" type="object"
string="SETUP" string="SETUP"
class="float-end btn-secondary" class="float-end btn-secondary"
attrs="{'invisible': [('payment_acquirer_id', '=', False)]}" attrs="{'invisible': [('payment_provider_id', '=', False)]}"
groups="base.group_system"/> groups="base.group_system"/>
</xpath> </xpath>
<xpath expr="//field[@name='inbound_payment_method_line_ids']/tree" position="attributes"> <xpath expr="//field[@name='inbound_payment_method_line_ids']/tree" position="attributes">
<attribute name="decoration-muted">payment_acquirer_state == 'disabled'</attribute> <attribute name="decoration-muted">payment_provider_state == 'disabled'</attribute>
</xpath> </xpath>
</field> </field>
</record> </record>
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<odoo> <odoo>
<menuitem action="payment.action_payment_acquirer" <menuitem action="payment.action_payment_provider"
id="payment_acquirer_menu" id="payment_provider_menu"
parent="account.root_payment_menu" parent="account.root_payment_menu"
sequence="10"/> sequence="10"/>
<menuitem action="payment.action_payment_icon" <menuitem action="payment.action_payment_icon"
@@ -48,7 +48,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div t-if="acquirers or tokens" id="payment_method" class="text-start col-md-13"> <div t-if="providers or tokens" id="payment_method" class="text-start col-md-13">
<t t-call="payment.checkout"/> <t t-call="payment.checkout"/>
</div> </div>
<div t-else="" class="alert alert-warning"> <div t-else="" class="alert alert-warning">
@@ -125,8 +125,8 @@
<xpath expr="//a[hasclass('close')]" position="after"> <xpath expr="//a[hasclass('close')]" position="after">
<t t-if="success == 'pay_invoice'"> <t t-if="success == 'pay_invoice'">
<t t-set="payment_tx_id" t-value="invoice.get_portal_last_transaction()"/> <t t-set="payment_tx_id" t-value="invoice.get_portal_last_transaction()"/>
<span t-if='payment_tx_id.acquirer_id.sudo().done_msg' t-out="payment_tx_id.acquirer_id.sudo().done_msg"/> <span t-if='payment_tx_id.provider_id.sudo().done_msg' t-out="payment_tx_id.provider_id.sudo().done_msg"/>
<div t-if="payment_tx_id.acquirer_id.sudo().pending_msg and payment_tx_id.provider == 'transfer' and invoice.ref"> <div t-if="payment_tx_id.provider_id.sudo().pending_msg and payment_tx_id.provider_code == 'transfer' and invoice.ref">
<b>Communication: </b><span t-esc='invoice.ref'/> <b>Communication: </b><span t-esc='invoice.ref'/>
</div> </div>
</t> </t>
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<odoo> <odoo>
<record id="payment_acquirer_form" model="ir.ui.view"> <record id="payment_provider_form" model="ir.ui.view">
<field name="name">payment.acquirer.form</field> <field name="name">payment.provider.form</field>
<field name="model">payment.acquirer</field> <field name="model">payment.provider</field>
<field name="inherit_id" ref="payment.payment_acquirer_form"/> <field name="inherit_id" ref="payment.payment_provider_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<group name="payment_followup" position="attributes"> <group name="payment_followup" position="attributes">
<attribute name="invisible"> <attribute name="invisible">
@@ -14,7 +14,7 @@
<group name="payment_followup" position="inside"> <group name="payment_followup" position="inside">
<field name="journal_id" <field name="journal_id"
context="{'default_type': 'bank'}" context="{'default_type': 'bank'}"
attrs="{'required': [('state', '!=', 'disabled'), ('provider', 'not in', ['none', 'transfer'])]}"/> attrs="{'required': [('state', '!=', 'disabled'), ('code', 'not in', ['none', 'transfer'])]}"/>
</group> </group>
</field> </field>
</record> </record>
@@ -13,7 +13,7 @@ class AccountPaymentRegister(models.TransientModel):
domain='''[ domain='''[
('id', 'in', suitable_payment_token_ids), ('id', 'in', suitable_payment_token_ids),
]''', ]''',
help="Note that tokens from acquirers set to only authorize transactions (instead of capturing the amount) are " help="Note that tokens from providers set to only authorize transactions (instead of capturing the amount) are "
"not available.") "not available.")
# == Display purpose fields == # == Display purpose fields ==
@@ -44,9 +44,9 @@ class AccountPaymentRegister(models.TransientModel):
wizard.suitable_payment_token_ids = self.env['payment.token'].sudo().search([ wizard.suitable_payment_token_ids = self.env['payment.token'].sudo().search([
('company_id', '=', wizard.company_id.id), ('company_id', '=', wizard.company_id.id),
('acquirer_id.capture_manually', '=', False), ('provider_id.capture_manually', '=', False),
('partner_id', 'in', related_partner_ids.ids), ('partner_id', 'in', related_partner_ids.ids),
('acquirer_id', '=', wizard.payment_method_line_id.payment_acquirer_id.id), ('provider_id', '=', wizard.payment_method_line_id.payment_provider_id.id),
]) ])
else: else:
wizard.suitable_payment_token_ids = [Command.clear()] wizard.suitable_payment_token_ids = [Command.clear()]
@@ -55,13 +55,13 @@ class AccountPaymentRegister(models.TransientModel):
def _compute_use_electronic_payment_method(self): def _compute_use_electronic_payment_method(self):
for wizard in self: for wizard in self:
# Get a list of all electronic payment method codes. # Get a list of all electronic payment method codes.
# These codes are comprised of the providers of each payment acquirer. # These codes are comprised of the codes of each payment provider.
codes = [key for key in dict(self.env['payment.acquirer']._fields['provider']._description_selection(self.env))] codes = [key for key in dict(self.env['payment.provider']._fields['code']._description_selection(self.env))]
wizard.use_electronic_payment_method = wizard.payment_method_code in codes wizard.use_electronic_payment_method = wizard.payment_method_code in codes
@api.onchange('can_edit_wizard', 'payment_method_line_id', 'journal_id') @api.onchange('can_edit_wizard', 'payment_method_line_id', 'journal_id')
def _compute_payment_token_id(self): def _compute_payment_token_id(self):
codes = [key for key in dict(self.env['payment.acquirer']._fields['provider']._description_selection(self.env))] codes = [key for key in dict(self.env['payment.provider']._fields['code']._description_selection(self.env))]
for wizard in self: for wizard in self:
related_partner_ids = ( related_partner_ids = (
wizard.partner_id wizard.partner_id
@@ -76,8 +76,8 @@ class AccountPaymentRegister(models.TransientModel):
wizard.payment_token_id = self.env['payment.token'].sudo().search([ wizard.payment_token_id = self.env['payment.token'].sudo().search([
('company_id', '=', wizard.company_id.id), ('company_id', '=', wizard.company_id.id),
('partner_id', 'in', related_partner_ids.ids), ('partner_id', 'in', related_partner_ids.ids),
('acquirer_id.capture_manually', '=', False), ('provider_id.capture_manually', '=', False),
('acquirer_id', '=', wizard.payment_method_line_id.payment_acquirer_id.id), ('provider_id', '=', wizard.payment_method_line_id.payment_provider_id.id),
], limit=1) ], limit=1)
else: else:
wizard.payment_token_id = False wizard.payment_token_id = False
@@ -26,7 +26,7 @@ class PaymentRefundWizard(models.TransientModel):
string="Refund Amount", compute='_compute_amount_to_refund', store=True, readonly=False string="Refund Amount", compute='_compute_amount_to_refund', store=True, readonly=False
) )
currency_id = fields.Many2one(string="Currency", related='transaction_id.currency_id') currency_id = fields.Many2one(string="Currency", related='transaction_id.currency_id')
support_refund = fields.Selection(related='transaction_id.acquirer_id.support_refund') support_refund = fields.Selection(related='transaction_id.provider_id.support_refund')
has_pending_refund = fields.Boolean( has_pending_refund = fields.Boolean(
string="Has a pending refund", compute='_compute_has_pending_refund' string="Has a pending refund", compute='_compute_has_pending_refund'
) )
@@ -14,7 +14,7 @@
<p> <p>
<strong>Warning!</strong> There is a refund pending for this payment. <strong>Warning!</strong> There is a refund pending for this payment.
Wait a moment for it to be processed. If the refund is still pending in a Wait a moment for it to be processed. If the refund is still pending in a
few minutes, please check your payment acquirer configuration. few minutes, please check your payment provider configuration.
</p> </p>
</div> </div>
<group> <group>
@@ -7,8 +7,8 @@
<field name="logo" type="base64" file="l10n_ar_website_sale/static/description/icon.png"/> <field name="logo" type="base64" file="l10n_ar_website_sale/static/description/icon.png"/>
<field name="domain" model="ir.config_parameter" eval="obj().env['ir.config_parameter'].sudo().get_param('web.base.url')"/> <field name="domain" model="ir.config_parameter" eval="obj().env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
</record> </record>
<function model="payment.acquirer" name="copy"> <function model="payment.provider" name="copy">
<value eval="[ref('payment.payment_acquirer_transfer')]"/> <value eval="[ref('payment.payment_provider_transfer')]"/>
<value eval="{'company_id': ref('l10n_ar.company_ri'), 'state': 'enabled'}"/> <value eval="{'company_id': ref('l10n_ar.company_ri'), 'state': 'enabled'}"/>
</function> </function>
<function model="product.pricelist" name="write"> <function model="product.pricelist" name="write">
+4 -4
View File
@@ -8,11 +8,11 @@ from . import wizards
from odoo import api, SUPERUSER_ID from odoo import api, SUPERUSER_ID
def setup_provider(cr, registry, provider_code): def setup_provider(cr, registry, code):
env = api.Environment(cr, SUPERUSER_ID, {}) env = api.Environment(cr, SUPERUSER_ID, {})
env['payment.acquirer']._setup_provider(provider_code) env['payment.provider']._setup_provider(code)
def reset_payment_acquirer(cr, registry, provider): def reset_payment_provider(cr, registry, code):
env = api.Environment(cr, SUPERUSER_ID, {}) env = api.Environment(cr, SUPERUSER_ID, {})
env['payment.acquirer']._remove_provider(provider) env['payment.provider']._remove_provider(code)
+6 -6
View File
@@ -1,21 +1,21 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
{ {
'name': 'Payment Acquirer', 'name': 'Payment Provider',
'version': '2.0', 'version': '2.0',
'category': 'Hidden', 'category': 'Hidden',
'summary': 'Base Module for Payment Acquirers', 'summary': 'Base Module for Payment Providers',
'description': """Payment Acquirer Base Module""", 'description': """Payment Provider Base Module""",
'depends': ['portal'], 'depends': ['portal'],
'data': [ 'data': [
'data/payment_icon_data.xml', 'data/payment_icon_data.xml',
'data/payment_acquirer_data.xml', 'data/payment_provider_data.xml',
'data/payment_cron.xml', 'data/payment_cron.xml',
'views/payment_portal_templates.xml', 'views/payment_portal_templates.xml',
'views/payment_templates.xml', 'views/payment_templates.xml',
'views/payment_acquirer_views.xml', 'views/payment_provider_views.xml',
'views/payment_icon_views.xml', 'views/payment_icon_views.xml',
'views/payment_transaction_views.xml', 'views/payment_transaction_views.xml',
'views/payment_token_views.xml', # Depends on `action_payment_transaction_linked_to_token` 'views/payment_token_views.xml', # Depends on `action_payment_transaction_linked_to_token`
@@ -43,7 +43,7 @@
'payment/static/src/js/post_processing.js', 'payment/static/src/js/post_processing.js',
], ],
'web.assets_backend': [ 'web.assets_backend': [
'payment/static/src/scss/payment_acquirer.scss', 'payment/static/src/scss/payment_provider.scss',
], ],
}, },
'license': 'LGPL-3', 'license': 'LGPL-3',
+41 -41
View File
@@ -39,7 +39,7 @@ class PaymentPortal(portal.CustomerPortal):
) )
def payment_pay( def payment_pay(
self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None, self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None,
acquirer_id=None, access_token=None, **kwargs provider_id=None, access_token=None, **kwargs
): ):
""" Display the payment form with optional filtering of payment options. """ Display the payment form with optional filtering of payment options.
@@ -58,7 +58,7 @@ class PaymentPortal(portal.CustomerPortal):
:param str currency_id: The desired currency, as a `res.currency` id :param str currency_id: The desired currency, as a `res.currency` id
:param str partner_id: The partner making the payment, as a `res.partner` id :param str partner_id: The partner making the payment, as a `res.partner` id
:param str company_id: The related company, as a `res.company` id :param str company_id: The related company, as a `res.company` id
:param str acquirer_id: The desired acquirer, as a `payment.acquirer` id :param str provider_id: The desired provider, as a `payment.provider` id
:param str access_token: The access token used to authenticate the partner :param str access_token: The access token used to authenticate the partner
:param dict kwargs: Optional data passed to helper methods. :param dict kwargs: Optional data passed to helper methods.
:return: The rendered checkout form :return: The rendered checkout form
@@ -66,8 +66,8 @@ class PaymentPortal(portal.CustomerPortal):
:raise: werkzeug.exceptions.NotFound if the access token is invalid :raise: werkzeug.exceptions.NotFound if the access token is invalid
""" """
# Cast numeric parameters as int or float and void them if their str value is malformed # Cast numeric parameters as int or float and void them if their str value is malformed
currency_id, acquirer_id, partner_id, company_id = tuple(map( currency_id, provider_id, partner_id, company_id = tuple(map(
self._cast_as_int, (currency_id, acquirer_id, partner_id, company_id) self._cast_as_int, (currency_id, provider_id, partner_id, company_id)
)) ))
amount = self._cast_as_float(amount) amount = self._cast_as_float(amount)
@@ -110,31 +110,31 @@ class PaymentPortal(portal.CustomerPortal):
if not currency or not currency.active: if not currency or not currency.active:
raise werkzeug.exceptions.NotFound # The currency must exist and be active raise werkzeug.exceptions.NotFound # The currency must exist and be active
# Select all acquirers and tokens that match the constraints # Select all providers and tokens that match the constraints
acquirers_sudo = request.env['payment.acquirer'].sudo()._get_compatible_acquirers( providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
company_id, partner_sudo.id, amount, currency_id=currency.id, **kwargs company_id, partner_sudo.id, amount, currency_id=currency.id, **kwargs
) # In sudo mode to read the fields of acquirers and partner (if not logged in) ) # In sudo mode to read the fields of providers and partner (if not logged in)
if acquirer_id in acquirers_sudo.ids: # Only keep the desired acquirer if it's suitable if provider_id in providers_sudo.ids: # Only keep the desired provider if it's suitable
acquirers_sudo = acquirers_sudo.browse(acquirer_id) providers_sudo = providers_sudo.browse(provider_id)
payment_tokens = request.env['payment.token'].search( payment_tokens = request.env['payment.token'].search(
[('acquirer_id', 'in', acquirers_sudo.ids), ('partner_id', '=', partner_sudo.id)] [('provider_id', 'in', providers_sudo.ids), ('partner_id', '=', partner_sudo.id)]
) if logged_in else request.env['payment.token'] ) if logged_in else request.env['payment.token']
# Compute the fees taken by acquirers supporting the feature # Compute the fees taken by providers supporting the feature
fees_by_acquirer = { fees_by_provider = {
acq_sudo: acq_sudo._compute_fees(amount, currency, partner_sudo.country_id) pro_sudo: pro_sudo._compute_fees(amount, currency, partner_sudo.country_id)
for acq_sudo in acquirers_sudo.filtered('fees_active') for pro_sudo in providers_sudo.filtered('fees_active')
} }
# Generate a new access token in case the partner id or the currency id was updated # Generate a new access token in case the partner id or the currency id was updated
access_token = payment_utils.generate_access_token(partner_sudo.id, amount, currency.id) access_token = payment_utils.generate_access_token(partner_sudo.id, amount, currency.id)
rendering_context = { rendering_context = {
'acquirers': acquirers_sudo, 'providers': providers_sudo,
'tokens': payment_tokens, 'tokens': payment_tokens,
'fees_by_acquirer': fees_by_acquirer, 'fees_by_provider': fees_by_provider,
'show_tokenize_input': self._compute_show_tokenize_input_mapping( 'show_tokenize_input': self._compute_show_tokenize_input_mapping(
acquirers_sudo, logged_in=logged_in, **kwargs providers_sudo, logged_in=logged_in, **kwargs
), ),
'reference_prefix': reference, 'reference_prefix': reference,
'amount': amount, 'amount': amount,
@@ -150,23 +150,23 @@ class PaymentPortal(portal.CustomerPortal):
return request.render(self._get_payment_page_template_xmlid(**kwargs), rendering_context) return request.render(self._get_payment_page_template_xmlid(**kwargs), rendering_context)
@staticmethod @staticmethod
def _compute_show_tokenize_input_mapping(acquirers_sudo, logged_in=False, **kwargs): def _compute_show_tokenize_input_mapping(providers_sudo, logged_in=False, **kwargs):
""" Determine for each acquirer whether the tokenization input should be shown or not. """ Determine for each provider whether the tokenization input should be shown or not.
:param recordset acquirers_sudo: The acquirers for which to determine whether the :param recordset providers_sudo: The providers for which to determine whether the
tokenization input should be shown or not, as a sudoed tokenization input should be shown or not, as a sudoed
`payment.acquirer` recordset. `payment.provider` recordset.
:param bool logged_in: Whether the user is logged in or not. :param bool logged_in: Whether the user is logged in or not.
:param dict kwargs: The optional data passed to the helper methods. :param dict kwargs: The optional data passed to the helper methods.
:return: The mapping of the computed value for each acquirer id. :return: The mapping of the computed value for each provider id.
:rtype: dict :rtype: dict
""" """
show_tokenize_input_mapping = {} show_tokenize_input_mapping = {}
for acquirer_sudo in acquirers_sudo: for provider_sudo in providers_sudo:
show_tokenize_input = acquirer_sudo.allow_tokenization \ show_tokenize_input = provider_sudo.allow_tokenization \
and not acquirer_sudo._is_tokenization_required(**kwargs) \ and not provider_sudo._is_tokenization_required(**kwargs) \
and logged_in and logged_in
show_tokenize_input_mapping[acquirer_sudo.id] = show_tokenize_input show_tokenize_input_mapping[provider_sudo.id] = show_tokenize_input
return show_tokenize_input_mapping return show_tokenize_input_mapping
def _get_payment_page_template_xmlid(self, **kwargs): def _get_payment_page_template_xmlid(self, **kwargs):
@@ -181,7 +181,7 @@ class PaymentPortal(portal.CustomerPortal):
:rtype: str :rtype: str
""" """
partner_sudo = request.env.user.partner_id # env.user is always sudoed partner_sudo = request.env.user.partner_id # env.user is always sudoed
acquirers_sudo = request.env['payment.acquirer'].sudo()._get_compatible_acquirers( providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
request.env.company.id, request.env.company.id,
partner_sudo.id, partner_sudo.id,
0., # There is no amount to pay with validation transactions. 0., # There is no amount to pay with validation transactions.
@@ -189,15 +189,15 @@ class PaymentPortal(portal.CustomerPortal):
is_validation=True, is_validation=True,
) )
# Get all partner's tokens for which acquirers are not disabled. # Get all partner's tokens for which providers are not disabled.
tokens_sudo = request.env['payment.token'].sudo().search([ tokens_sudo = request.env['payment.token'].sudo().search([
('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id]), ('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id]),
('acquirer_id.state', 'in', ['enabled', 'test']), ('provider_id.state', 'in', ['enabled', 'test']),
]) ])
access_token = payment_utils.generate_access_token(partner_sudo.id, None, None) access_token = payment_utils.generate_access_token(partner_sudo.id, None, None)
rendering_context = { rendering_context = {
'acquirers': acquirers_sudo, 'providers': providers_sudo,
'tokens': tokens_sudo, 'tokens': tokens_sudo,
'reference_prefix': payment_utils.singularize_reference_prefix(prefix='V'), 'reference_prefix': payment_utils.singularize_reference_prefix(prefix='V'),
'partner_id': partner_sudo.id, 'partner_id': partner_sudo.id,
@@ -252,7 +252,7 @@ class PaymentPortal(portal.CustomerPortal):
""" Create a draft transaction based on the payment context and return it. """ Create a draft transaction based on the payment context and return it.
:param int payment_option_id: The payment option handling the transaction, as a :param int payment_option_id: The payment option handling the transaction, as a
`payment.acquirer` id or a `payment.token` id `payment.provider` id or a `payment.token` id
:param str reference_prefix: The custom prefix to compute the full reference :param str reference_prefix: The custom prefix to compute the full reference
:param float|None amount: The amount to pay in the given currency. :param float|None amount: The amount to pay in the given currency.
None if in a payment method validation operation None if in a payment method validation operation
@@ -272,13 +272,13 @@ class PaymentPortal(portal.CustomerPortal):
""" """
# Prepare create values # Prepare create values
if flow in ['redirect', 'direct']: # Direct payment or payment with redirection if flow in ['redirect', 'direct']: # Direct payment or payment with redirection
acquirer_sudo = request.env['payment.acquirer'].sudo().browse(payment_option_id) provider_sudo = request.env['payment.provider'].sudo().browse(payment_option_id)
token_id = None token_id = None
tokenize = bool( tokenize = bool(
# Don't tokenize if the user tried to force it through the browser's developer tools # Don't tokenize if the user tried to force it through the browser's developer tools
acquirer_sudo.allow_tokenization provider_sudo.allow_tokenization
# Token is only created if required by the flow or requested by the user # Token is only created if required by the flow or requested by the user
and (acquirer_sudo._is_tokenization_required(**kwargs) or tokenization_requested) and (provider_sudo._is_tokenization_required(**kwargs) or tokenization_requested)
) )
elif flow == 'token': # Payment by token elif flow == 'token': # Payment by token
token_sudo = request.env['payment.token'].sudo().browse(payment_option_id) token_sudo = request.env['payment.token'].sudo().browse(payment_option_id)
@@ -290,7 +290,7 @@ class PaymentPortal(portal.CustomerPortal):
if partner_sudo.commercial_partner_id != token_sudo.partner_id.commercial_partner_id: if partner_sudo.commercial_partner_id != token_sudo.partner_id.commercial_partner_id:
raise AccessError(_("You do not have access to this payment token.")) raise AccessError(_("You do not have access to this payment token."))
acquirer_sudo = token_sudo.acquirer_id provider_sudo = token_sudo.provider_id
token_id = payment_option_id token_id = payment_option_id
tokenize = False tokenize = False
else: else:
@@ -299,18 +299,18 @@ class PaymentPortal(portal.CustomerPortal):
) )
reference = request.env['payment.transaction']._compute_reference( reference = request.env['payment.transaction']._compute_reference(
acquirer_sudo.provider, provider_sudo.code,
prefix=reference_prefix, prefix=reference_prefix,
**(custom_create_values or {}), **(custom_create_values or {}),
**kwargs **kwargs
) )
if is_validation: # Acquirers determine the amount and currency in validation operations if is_validation: # Providers determine the amount and currency in validation operations
amount = acquirer_sudo._get_validation_amount() amount = provider_sudo._get_validation_amount()
currency_id = acquirer_sudo._get_validation_currency().id currency_id = provider_sudo._get_validation_currency().id
# Create the transaction # Create the transaction
tx_sudo = request.env['payment.transaction'].sudo().create({ tx_sudo = request.env['payment.transaction'].sudo().create({
'acquirer_id': acquirer_sudo.id, 'provider_id': provider_sudo.id,
'reference': reference, 'reference': reference,
'amount': amount, 'amount': amount,
'currency_id': currency_id, 'currency_id': currency_id,
@@ -338,7 +338,7 @@ class PaymentPortal(portal.CustomerPortal):
The generic landing route requires the tx id and access token to be provided since there is The generic landing route requires the tx id and access token to be provided since there is
no document to rely on. The access token is recomputed in case we are dealing with a no document to rely on. The access token is recomputed in case we are dealing with a
validation transaction (acquirer-specific amount and currency). validation transaction (provider-specific amount and currency).
:param recordset tx_sudo: The transaction whose landing routes to update, as a :param recordset tx_sudo: The transaction whose landing routes to update, as a
`payment.transaction` record. `payment.transaction` record.
@@ -58,11 +58,11 @@ class PaymentPostProcessing(http.Controller):
for tx in monitored_txs: for tx in monitored_txs:
display_message = None display_message = None
if tx.state == 'pending': if tx.state == 'pending':
display_message = tx.acquirer_id.pending_msg display_message = tx.provider_id.pending_msg
elif tx.state == 'done': elif tx.state == 'done':
display_message = tx.acquirer_id.done_msg display_message = tx.provider_id.done_msg
elif tx.state == 'cancel': elif tx.state == 'cancel':
display_message = tx.acquirer_id.cancel_msg display_message = tx.provider_id.cancel_msg
display_values_list.append({ display_values_list.append({
'display_message': display_message, 'display_message': display_message,
**tx._get_post_processing_values(), **tx._get_post_processing_values(),
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1"> <odoo noupdate="1">
<record id="payment_acquirer_adyen" model="payment.acquirer"> <record id="payment_provider_adyen" model="payment.provider">
<field name="name">Adyen</field> <field name="name">Adyen</field>
<field name="display_as">Credit Card (powered by Adyen)</field> <field name="display_as">Credit Card (powered by Adyen)</field>
<field name="image_128" type="base64" file="payment_adyen/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_adyen/static/description/icon.png"/>
@@ -20,7 +20,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_aps" model="payment.acquirer"> <record id="payment_provider_aps" model="payment.provider">
<field name="name">Amazon Payment Services</field> <field name="name">Amazon Payment Services</field>
<field name="display_as">Amazon Payment Services</field> <field name="display_as">Amazon Payment Services</field>
<field name="image_128" type="base64" file="payment_aps/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_aps/static/description/icon.png"/>
@@ -35,7 +35,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_asiapay" model="payment.acquirer"> <record id="payment_provider_asiapay" model="payment.provider">
<field name="name">Asiapay</field> <field name="name">Asiapay</field>
<field name="display_as">Credit Card (powered by Asiapay)</field> <field name="display_as">Credit Card (powered by Asiapay)</field>
<field name="image_128" type="base64" file="payment_asiapay/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_asiapay/static/description/icon.png"/>
@@ -49,7 +49,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_authorize" model="payment.acquirer"> <record id="payment_provider_authorize" model="payment.provider">
<field name="name">Authorize.net</field> <field name="name">Authorize.net</field>
<field name="display_as">Credit Card (powered by Authorize)</field> <field name="display_as">Credit Card (powered by Authorize)</field>
<field name="image_128" <field name="image_128"
@@ -68,7 +68,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_buckaroo" model="payment.acquirer"> <record id="payment_provider_buckaroo" model="payment.provider">
<field name="name">Buckaroo</field> <field name="name">Buckaroo</field>
<field name="display_as">Credit Card (powered by Buckaroo)</field> <field name="display_as">Credit Card (powered by Buckaroo)</field>
<field name="image_128" <field name="image_128"
@@ -86,14 +86,14 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_demo" model="payment.acquirer"> <record id="payment_provider_demo" model="payment.provider">
<field name="name">Demo</field> <field name="name">Demo</field>
<field name="sequence">40</field> <field name="sequence">40</field>
<field name="image_128" type="base64" file="payment_demo/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_demo/static/description/icon.png"/>
<field name="module_id" ref="base.module_payment_demo"/> <field name="module_id" ref="base.module_payment_demo"/>
</record> </record>
<record id="payment_acquirer_flutterwave" model="payment.acquirer"> <record id="payment_provider_flutterwave" model="payment.provider">
<field name="name">Flutterwave</field> <field name="name">Flutterwave</field>
<field name="image_128" <field name="image_128"
type="base64" type="base64"
@@ -111,7 +111,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_mercado_pago" model="payment.acquirer"> <record id="payment_provider_mercado_pago" model="payment.provider">
<field name="name">Mercado Pago</field> <field name="name">Mercado Pago</field>
<field name="display_as">Credit Card (powered by Mercado Pago)</field> <field name="display_as">Credit Card (powered by Mercado Pago)</field>
<field name="image_128" <field name="image_128"
@@ -132,7 +132,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_mollie" model="payment.acquirer"> <record id="payment_provider_mollie" model="payment.provider">
<field name="name">Mollie</field> <field name="name">Mollie</field>
<field name="image_128" type="base64" file="payment_mollie/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_mollie/static/description/icon.png"/>
<field name="module_id" ref="base.module_payment_mollie"/> <field name="module_id" ref="base.module_payment_mollie"/>
@@ -155,7 +155,7 @@
</record> </record>
<record id="payment_acquirer_paypal" model="payment.acquirer"> <record id="payment_provider_paypal" model="payment.provider">
<field name="name">PayPal</field> <field name="name">PayPal</field>
<field name="image_128" type="base64" file="payment_paypal/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_paypal/static/description/icon.png"/>
<field name="module_id" ref="base.module_payment_paypal"/> <field name="module_id" ref="base.module_payment_paypal"/>
@@ -173,7 +173,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_razorpay" model="payment.acquirer"> <record id="payment_provider_razorpay" model="payment.provider">
<field name="name">Razorpay</field> <field name="name">Razorpay</field>
<field name="display_as">Credit &amp; Debit Card, UPI (Powered by Razorpay)</field> <field name="display_as">Credit &amp; Debit Card, UPI (Powered by Razorpay)</field>
<field name="image_128" type="base64" file="payment_razorpay/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_razorpay/static/description/icon.png"/>
@@ -190,7 +190,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_sepa_direct_debit" model="payment.acquirer"> <record id="payment_provider_sepa_direct_debit" model="payment.provider">
<field name="name">SEPA Direct Debit</field> <field name="name">SEPA Direct Debit</field>
<field name="sequence">20</field> <field name="sequence">20</field>
<field name="image_128" <field name="image_128"
@@ -199,7 +199,7 @@
<field name="module_id" ref="base.module_payment_sepa_direct_debit"/> <field name="module_id" ref="base.module_payment_sepa_direct_debit"/>
</record> </record>
<record id="payment_acquirer_sips" model="payment.acquirer"> <record id="payment_provider_sips" model="payment.provider">
<field name="name">Sips</field> <field name="name">Sips</field>
<field name="display_as">Credit Card (powered by Sips)</field> <field name="display_as">Credit Card (powered by Sips)</field>
<field name="image_128" type="base64" file="payment_sips/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_sips/static/description/icon.png"/>
@@ -219,7 +219,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_stripe" model="payment.acquirer"> <record id="payment_provider_stripe" model="payment.provider">
<field name="name">Stripe</field> <field name="name">Stripe</field>
<field name="display_as">Credit &amp; Debit Card</field> <field name="display_as">Credit &amp; Debit Card</field>
<field name="image_128" type="base64" file="payment_stripe/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_stripe/static/description/icon.png"/>
@@ -240,7 +240,7 @@
])]"/> ])]"/>
</record> </record>
<record id="payment_acquirer_transfer" model="payment.acquirer"> <record id="payment_provider_transfer" model="payment.provider">
<field name="name">Wire Transfer</field> <field name="name">Wire Transfer</field>
<field name="sequence">30</field> <field name="sequence">30</field>
<field name="image_128" <field name="image_128"
+1 -1
View File
@@ -2,7 +2,7 @@
from . import ir_http from . import ir_http
from . import ir_ui_view from . import ir_ui_view
from . import payment_acquirer from . import payment_provider
from . import payment_icon from . import payment_icon
from . import payment_token from . import payment_token
from . import payment_transaction from . import payment_transaction
+4 -4
View File
@@ -8,9 +8,9 @@ class IrUiView(models.Model):
_inherit = 'ir.ui.view' _inherit = 'ir.ui.view'
@api.ondelete(at_uninstall=False) @api.ondelete(at_uninstall=False)
def _unlink_if_not_referenced_by_acquirer(self): def _unlink_if_not_referenced_by_provider(self):
referencing_acquirers_sudo = self.env['payment.acquirer'].sudo().search([ referencing_providers_sudo = self.env['payment.provider'].sudo().search([
'|', ('redirect_form_view_id', 'in', self.ids), ('inline_form_view_id', 'in', self.ids) '|', ('redirect_form_view_id', 'in', self.ids), ('inline_form_view_id', 'in', self.ids)
]) # In sudo mode to allow non-admin users (e.g., Website designers) to read the view ids. ]) # In sudo mode to allow non-admin users (e.g., Website designers) to read the view ids.
if referencing_acquirers_sudo: if referencing_providers_sudo:
raise UserError(_("You cannot delete a view that is used by a payment acquirer.")) raise UserError(_("You cannot delete a view that is used by a payment provider."))
+3 -3
View File
@@ -9,9 +9,9 @@ class PaymentIcon(models.Model):
_order = 'sequence, name' _order = 'sequence, name'
name = fields.Char(string="Name") name = fields.Char(string="Name")
acquirer_ids = fields.Many2many( provider_ids = fields.Many2many(
string="Acquirers", comodel_name='payment.acquirer', string="Providers", comodel_name='payment.provider',
help="The list of acquirers supporting this payment icon") help="The list of providers supporting this payment icon")
image = fields.Image( image = fields.Image(
string="Image", max_width=64, max_height=64, string="Image", max_width=64, max_height=64,
help="This field holds the image used for this payment icon, limited to 64x64 px") help="This field holds the image used for this payment icon, limited to 64x64 px")
@@ -11,9 +11,9 @@ from odoo.osv import expression
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class PaymentAcquirer(models.Model): class PaymentProvider(models.Model):
_name = 'payment.acquirer' _name = 'payment.provider'
_description = 'Payment Acquirer' _description = 'Payment Provider'
_order = 'module_state, state desc, sequence, name' _order = 'module_state, state desc, sequence, name'
def _valid_field_parameter(self, field, name): def _valid_field_parameter(self, field, name):
@@ -22,18 +22,18 @@ class PaymentAcquirer(models.Model):
# Configuration fields # Configuration fields
name = fields.Char(string="Name", required=True, translate=True) name = fields.Char(string="Name", required=True, translate=True)
sequence = fields.Integer(string="Sequence", help="Define the display order") sequence = fields.Integer(string="Sequence", help="Define the display order")
provider = fields.Selection( code = fields.Selection(
string="Provider", help="The Payment Service Provider to use with this acquirer", string="Code", help="The Payment Service Code to use with this provider",
selection=[('none', "No Provider Set")], default='none', required=True) selection=[('none', "No Provider Set")], default='none', required=True)
state = fields.Selection( state = fields.Selection(
string="State", string="State",
help="In test mode, a fake payment is processed through a test payment interface.\n" help="In test mode, a fake payment is processed through a test payment interface.\n"
"This mode is advised when setting up the acquirer.", "This mode is advised when setting up the provider.",
selection=[('disabled', "Disabled"), ('enabled', "Enabled"), ('test', "Test Mode")], selection=[('disabled', "Disabled"), ('enabled', "Enabled"), ('test', "Test Mode")],
default='disabled', required=True, copy=False) default='disabled', required=True, copy=False)
is_published = fields.Boolean( is_published = fields.Boolean(
string="Published", string="Published",
help="Whether the acquirer is visible on the website or not. Tokens remain functional but " help="Whether the provider is visible on the website or not. Tokens remain functional but "
"are only visible on manage forms.", "are only visible on manage forms.",
) )
company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others) company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others)
@@ -49,7 +49,7 @@ class PaymentAcquirer(models.Model):
string="Allow Saving Payment Methods", string="Allow Saving Payment Methods",
help="This controls whether customers can save their payment methods as payment tokens.\n" help="This controls whether customers can save their payment methods as payment tokens.\n"
"A payment token is an anonymous link to the payment method details saved in the\n" "A payment token is an anonymous link to the payment method details saved in the\n"
"acquirer's database, allowing the customer to reuse it for a next purchase.") "provider's database, allowing the customer to reuse it for a next purchase.")
capture_manually = fields.Boolean( capture_manually = fields.Boolean(
string="Capture Amount Manually", string="Capture Amount Manually",
help="Capture the amount from Odoo, when the delivery is completed.\n" help="Capture the amount from Odoo, when the delivery is completed.\n"
@@ -86,7 +86,7 @@ class PaymentAcquirer(models.Model):
available_country_ids = fields.Many2many( available_country_ids = fields.Many2many(
string="Countries", string="Countries",
comodel_name='res.country', comodel_name='res.country',
help="The countries in which this payment acquirer is available. Leave blank to make it " help="The countries in which this payment provider is available. Leave blank to make it "
"available in all countries.", "available in all countries.",
relation='payment_country_rel', relation='payment_country_rel',
column1='payment_id', column1='payment_id',
@@ -94,7 +94,7 @@ class PaymentAcquirer(models.Model):
) )
maximum_amount = fields.Monetary( maximum_amount = fields.Monetary(
string="Maximum Amount", string="Maximum Amount",
help="The maximum payment amount that this payment acquirer is available for. Leave blank " help="The maximum payment amount that this payment provider is available for. Leave blank "
"to make it available for any payment amount.", "to make it available for any payment amount.",
currency_field='main_currency_id', currency_field='main_currency_id',
) )
@@ -108,7 +108,7 @@ class PaymentAcquirer(models.Model):
# Message fields # Message fields
display_as = fields.Char( display_as = fields.Char(
string="Displayed as", help="Description of the acquirer for customers", string="Displayed as", help="Description of the provider for customers",
translate=True) translate=True)
pre_msg = fields.Html( pre_msg = fields.Html(
string="Help Message", help="The message displayed to explain and help the payment process", string="Help Message", help="The message displayed to explain and help the payment process",
@@ -178,26 +178,26 @@ class PaymentAcquirer(models.Model):
@api.depends('state', 'module_state') @api.depends('state', 'module_state')
def _compute_color(self): def _compute_color(self):
""" Update the color of the kanban card based on the state of the acquirer. """ Update the color of the kanban card based on the state of the provider.
:return: None :return: None
""" """
for acquirer in self: for provider in self:
if acquirer.module_id and not acquirer.module_state == 'installed': if provider.module_id and not provider.module_state == 'installed':
acquirer.color = 4 # blue provider.color = 4 # blue
elif acquirer.state == 'disabled': elif provider.state == 'disabled':
acquirer.color = 3 # yellow provider.color = 3 # yellow
elif acquirer.state == 'test': elif provider.state == 'test':
acquirer.color = 2 # orange provider.color = 2 # orange
elif acquirer.state == 'enabled': elif provider.state == 'enabled':
acquirer.color = 7 # green provider.color = 7 # green
@api.depends('provider') @api.depends('code')
def _compute_view_configuration_fields(self): def _compute_view_configuration_fields(self):
""" Compute view configuration fields based on the provider. """ Compute view configuration fields based on the provider.
By default, all fields are set to `True`. By default, all fields are set to `True`.
For an acquirer to hide generic elements (pages, fields) in a view, it must override this For a provider to hide generic elements (pages, fields) in a view, it must override this
method and set their corresponding view configuration field to `False`. method and set their corresponding view configuration field to `False`.
:return: None :return: None
@@ -217,7 +217,7 @@ class PaymentAcquirer(models.Model):
def _compute_feature_support_fields(self): def _compute_feature_support_fields(self):
""" Compute the feature support fields. """ Compute the feature support fields.
For an acquirer to support one or more additional feature, it must override this method. For an provider to support one or more additional feature, it must override this method.
:return: None :return: None
""" """
@@ -233,7 +233,7 @@ class PaymentAcquirer(models.Model):
@api.onchange('state') @api.onchange('state')
def _onchange_state_switch_is_published(self): def _onchange_state_switch_is_published(self):
""" Automatically publish or unpublish the acquirer depending on its state. """ Automatically publish or unpublish the provider depending on its state.
:return: None :return: None
""" """
@@ -241,9 +241,9 @@ class PaymentAcquirer(models.Model):
@api.onchange('state') @api.onchange('state')
def _onchange_state_warn_before_disabling_tokens(self): def _onchange_state_warn_before_disabling_tokens(self):
""" Display a warning about the consequences of disabling an acquirer. """ Display a warning about the consequences of disabling an provider.
Let the user know that tokens related to an acquirer get archived if it is disabled or if Let the user know that tokens related to an provider get archived if it is disabled or if
its state is changed from 'test' to 'enabled' and vice versa. its state is changed from 'test' to 'enabled' and vice versa.
:return: The warning message in a client action. :return: The warning message in a client action.
@@ -253,7 +253,7 @@ class PaymentAcquirer(models.Model):
if self._origin.state in ('test', 'enabled') and self._origin.state != self.state: if self._origin.state in ('test', 'enabled') and self._origin.state != self.state:
related_tokens = self.env['payment.token'].search( related_tokens = self.env['payment.token'].search(
[('acquirer_id', '=', self._origin.id)] [('provider_id', '=', self._origin.id)]
) )
if related_tokens: if related_tokens:
return { return {
@@ -261,7 +261,7 @@ class PaymentAcquirer(models.Model):
'title': _("Warning"), 'title': _("Warning"),
'message': _( 'message': _(
"This action will also archive %s tokens that are registered with this " "This action will also archive %s tokens that are registered with this "
"acquirer. Archiving tokens is irreversible.", len(related_tokens) "provider. Archiving tokens is irreversible.", len(related_tokens)
) )
} }
} }
@@ -277,25 +277,25 @@ class PaymentAcquirer(models.Model):
:return None :return None
""" """
for acquirer in self: for provider in self:
if any(not 0 <= fee < 100 for fee in (acquirer.fees_dom_var, acquirer.fees_int_var)): if any(not 0 <= fee < 100 for fee in (provider.fees_dom_var, provider.fees_int_var)):
raise ValidationError(_("Variable fees must always be positive and below 100%.")) raise ValidationError(_("Variable fees must always be positive and below 100%."))
#=== CRUD METHODS ===# #=== CRUD METHODS ===#
@api.model_create_multi @api.model_create_multi
def create(self, values_list): def create(self, values_list):
acquirers = super().create(values_list) providers = super().create(values_list)
acquirers._check_required_if_provider() providers._check_required_if_provider()
return acquirers return providers
def write(self, values): def write(self, values):
# Handle acquirer disabling. # Handle provider disabling.
if 'state' in values: if 'state' in values:
state_changed_acquirers = self.filtered( state_changed_providers = self.filtered(
lambda acq: acq.state not in ('disabled', values['state']) lambda p: p.state not in ('disabled', values['state'])
) # Don't handle acquirers being enabled or whose state is not updated. ) # Don't handle providers being enabled or whose state is not updated.
state_changed_acquirers._handle_state_change() state_changed_providers._handle_state_change()
result = super().write(values) result = super().write(values)
self._check_required_if_provider() self._check_required_if_provider()
@@ -303,23 +303,23 @@ class PaymentAcquirer(models.Model):
return result return result
def _check_required_if_provider(self): def _check_required_if_provider(self):
""" Check that acquirer-specific required fields have been filled. """ Check that provider-specific required fields have been filled.
The fields that have the `required_if_provider="<provider>"` attribute are made required The fields that have the `required_if_provider="<provider>"` attribute are made required
for all payment.acquirer records with the `provider` field equal to <provider> and with the for all payment.provider records with the `code` field equal to <provider> and with the
`state` field equal to 'enabled' or 'test'. `state` field equal to 'enabled' or 'test'.
Acquirer-specific views should make the form fields required under the same conditions. Provider-specific views should make the form fields required under the same conditions.
:return: None :return: None
:raise ValidationError: if an acquirer-specific required field is empty :raise ValidationError: if a provider-specific required field is empty
""" """
field_names = [] field_names = []
enabled_acquirers = self.filtered(lambda acq: acq.state in ['enabled', 'test']) enabled_providers = self.filtered(lambda p: p.state in ['enabled', 'test'])
for name, field in self._fields.items(): for name, field in self._fields.items():
required_provider = getattr(field, 'required_if_provider', None) required_provider = getattr(field, 'required_if_provider', None)
if required_provider and any( if required_provider and any(
required_provider == acquirer.provider and not acquirer[name] required_provider == provider.code and not provider[name]
for acquirer in enabled_acquirers for provider in enabled_providers
): ):
ir_field = self.env['ir.model.fields']._get(self._name, name) ir_field = self.env['ir.model.fields']._get(self._name, name)
field_names.append(ir_field.field_description) field_names.append(ir_field.field_description)
@@ -329,16 +329,16 @@ class PaymentAcquirer(models.Model):
) )
def _handle_state_change(self): def _handle_state_change(self):
""" Archive all the payment tokens linked to these acquirers. """ Archive all the payment tokens linked to these providers.
:return: None :return: None
""" """
self.env['payment.token'].search([('acquirer_id', 'in', self.ids)]).write({'active': False}) self.env['payment.token'].search([('provider_id', 'in', self.ids)]).write({'active': False})
#=== ACTION METHODS ===# #=== ACTION METHODS ===#
def button_immediate_install(self): def button_immediate_install(self):
""" Install the acquirer's module and reload the page. """ Install the provider's module and reload the page.
Note: self.ensure_one() Note: self.ensure_one()
@@ -353,40 +353,40 @@ class PaymentAcquirer(models.Model):
} }
def action_toggle_is_published(self): def action_toggle_is_published(self):
""" Toggle the acquirer's is_published state. """ Toggle the provider's is_published state.
:return: none :return: none
:raise UserError: If the acquirer is disabled. :raise UserError: If the provider is disabled.
""" """
if self.state != 'disabled': if self.state != 'disabled':
self.is_published = not self.is_published self.is_published = not self.is_published
else: else:
raise UserError(_("You cannot publish a disabled acquirer.")) raise UserError(_("You cannot publish a disabled provider."))
#=== BUSINESS METHODS ===# #=== BUSINESS METHODS ===#
@api.model @api.model
def _get_compatible_acquirers( def _get_compatible_providers(
self, company_id, partner_id, amount, currency_id=None, force_tokenization=False, self, company_id, partner_id, amount, currency_id=None, force_tokenization=False,
is_express_checkout=False, is_validation=False, **kwargs is_express_checkout=False, is_validation=False, **kwargs
): ):
""" Select and return the acquirers matching the criteria. """ Select and return the providers matching the criteria.
The base criteria are that acquirers must not be disabled, be in the company that is The base criteria are that providers must not be disabled, be in the company that is
provided, and support the country of the partner if it exists. provided, and support the country of the partner if it exists.
:param int company_id: The company to which acquirers must belong, as a `res.company` id :param int company_id: The company to which providers must belong, as a `res.company` id
:param int partner_id: The partner making the payment, as a `res.partner` id :param int partner_id: The partner making the payment, as a `res.partner` id
:param float amount: The amount to pay, `0` for validation transactions. :param float amount: The amount to pay, `0` for validation transactions.
:param int currency_id: The payment currency if known beforehand, as a `res.currency` id :param int currency_id: The payment currency if known beforehand, as a `res.currency` id
:param bool force_tokenization: Whether only acquirers allowing tokenization can be matched :param bool force_tokenization: Whether only providers allowing tokenization can be matched
:param bool is_express_checkout: Whether the payment is made through express checkout. :param bool is_express_checkout: Whether the payment is made through express checkout.
:param bool is_validation: Whether the operation is a validation :param bool is_validation: Whether the operation is a validation
:param dict kwargs: Optional data. This parameter is not used here :param dict kwargs: Optional data. This parameter is not used here
:return: The compatible acquirers :return: The compatible providers
:rtype: recordset of `payment.acquirer` :rtype: recordset of `payment.provider`
""" """
# Compute the base domain for compatible acquirers # Compute the base domain for compatible providers
domain = ['&', ('state', 'in', ['enabled', 'test']), ('company_id', '=', company_id)] domain = ['&', ('state', 'in', ['enabled', 'test']), ('company_id', '=', company_id)]
# Handle the is_published state. # Handle the is_published state.
@@ -427,8 +427,8 @@ class PaymentAcquirer(models.Model):
if is_express_checkout: if is_express_checkout:
domain = expression.AND([domain, [('allow_express_checkout', '=', True)]]) domain = expression.AND([domain, [('allow_express_checkout', '=', True)]])
compatible_acquirers = self.env['payment.acquirer'].search(domain) compatible_providers = self.env['payment.provider'].search(domain)
return compatible_acquirers return compatible_providers
def _is_tokenization_required(self, **kwargs): def _is_tokenization_required(self, **kwargs):
""" Return whether tokenizing the transaction is required given its context. """ Return whether tokenizing the transaction is required given its context.
@@ -445,7 +445,7 @@ class PaymentAcquirer(models.Model):
def _should_build_inline_form(self, is_validation=False): def _should_build_inline_form(self, is_validation=False):
""" Return whether the inline form should be instantiated if it exists. """ Return whether the inline form should be instantiated if it exists.
For an acquirer to handle both direct payments and payment with redirection, it should For a provider to handle both direct payments and payment with redirection, it should
override this method and return whether the inline form should be instantiated (i.e. if the override this method and return whether the inline form should be instantiated (i.e. if the
payment should be direct) based on the operation (online payment or validation). payment should be direct) based on the operation (online payment or validation).
@@ -465,7 +465,7 @@ class PaymentAcquirer(models.Model):
of `fixed` and `variable` is taken either from the domestic (dom) or international (int) of `fixed` and `variable` is taken either from the domestic (dom) or international (int)
field depending on whether the country matches the company's country. field depending on whether the country matches the company's country.
For an acquirer to base the computation on different variables, or to use a different For a provider to base the computation on different variables, or to use a different
formula, it must override this method and return the resulting fees as a float. formula, it must override this method and return the resulting fees as a float.
:param float amount: The amount to pay for the transaction :param float amount: The amount to pay for the transaction
@@ -490,7 +490,7 @@ class PaymentAcquirer(models.Model):
def _get_validation_amount(self): def _get_validation_amount(self):
""" Get the amount to transfer in a payment method validation operation. """ Get the amount to transfer in a payment method validation operation.
For an acquirer to support tokenization, it must override this method and return the amount For a provider to support tokenization, it must override this method and return the amount
to be transferred in a payment method validation operation *if the validation amount is not to be transferred in a payment method validation operation *if the validation amount is not
null*. null*.
@@ -505,7 +505,7 @@ class PaymentAcquirer(models.Model):
def _get_validation_currency(self): def _get_validation_currency(self):
""" Get the currency of the transfer in a payment method validation operation. """ Get the currency of the transfer in a payment method validation operation.
For an acquirer to support tokenization, it must override this method and return the For a provider to support tokenization, it must override this method and return the
currency to be used in a payment method validation operation *if the validation amount is currency to be used in a payment method validation operation *if the validation amount is
not null*. not null*.
@@ -520,7 +520,7 @@ class PaymentAcquirer(models.Model):
def _get_redirect_form_view(self, is_validation=False): def _get_redirect_form_view(self, is_validation=False):
""" Return the view of the template used to render the redirect form. """ Return the view of the template used to render the redirect form.
For an acquirer to return a different view depending on whether the operation is a For a provider to return a different view depending on whether the operation is a
validation, it must override this method and return the appropriate view. validation, it must override this method and return the appropriate view.
Note: self.ensure_one() Note: self.ensure_one()
@@ -550,8 +550,8 @@ class PaymentAcquirer(models.Model):
:param str provider_code: The code of the provider to setup. :param str provider_code: The code of the provider to setup.
:return: None :return: None
""" """
acquirers = self.search([('provider', '=', provider_code)]) providers = self.search([('provider', '=', provider_code)])
acquirers.write({ providers.write({
'provider': 'none', 'provider': 'none',
'state': 'disabled', 'state': 'disabled',
}) })
@@ -561,7 +561,7 @@ class PaymentAcquirer(models.Model):
self.flush_model() self.flush_model()
self.invalidate_model() self.invalidate_model()
self.env.cr.execute(""" self.env.cr.execute("""
UPDATE payment_acquirer SET state = 'disabled' UPDATE payment_provider SET state = 'disabled'
WHERE state NOT IN ('test', 'disabled') WHERE state NOT IN ('test', 'disabled')
""") """)
@@ -573,8 +573,8 @@ class PaymentAcquirer(models.Model):
self.flush_model() self.flush_model()
self.invalidate_model() self.invalidate_model()
query = sql.SQL(""" query = sql.SQL("""
UPDATE payment_acquirer UPDATE payment_provider
SET ({fields}) = ROW({vals}) SET ({fields}) = ROW({vals})
WHERE provider = %s WHERE code = %s
""").format(fields=sql.SQL(','.join(fields)), vals=sql.SQL(', '.join(['NULL'] * len(fields)))) """).format(fields=sql.SQL(','.join(fields)), vals=sql.SQL(', '.join(['NULL'] * len(fields))))
self.env.cr.execute(query, (provider, )) self.env.cr.execute(query, (provider, ))
+17 -17
View File
@@ -11,18 +11,18 @@ class PaymentToken(models.Model):
_order = 'partner_id, id desc' _order = 'partner_id, id desc'
_description = 'Payment Token' _description = 'Payment Token'
acquirer_id = fields.Many2one( provider_id = fields.Many2one(
string="Acquirer Account", comodel_name='payment.acquirer', required=True) string="provider Account", comodel_name='payment.provider', required=True)
provider = fields.Selection(related='acquirer_id.provider') provider_code = fields.Selection(related='provider_id.code')
payment_details = fields.Char( payment_details = fields.Char(
string="Payment Details", help="The clear part of the payment method's payment details.", string="Payment Details", help="The clear part of the payment method's payment details.",
) )
partner_id = fields.Many2one(string="Partner", comodel_name='res.partner', required=True) partner_id = fields.Many2one(string="Partner", comodel_name='res.partner', required=True)
company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others) company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others)
related='acquirer_id.company_id', store=True, index=True) related='provider_id.company_id', store=True, index=True)
acquirer_ref = fields.Char( provider_ref = fields.Char(
string="Acquirer Reference", help="The acquirer reference of the token of the transaction", string="Provider Reference", help="The provider reference of the token of the transaction",
required=True) # This is not the same thing as the acquirer reference of the transaction required=True) # This is not the same thing as the provider reference of the transaction
transaction_ids = fields.One2many( transaction_ids = fields.One2many(
string="Payment Transactions", comodel_name='payment.transaction', inverse_name='token_id') string="Payment Transactions", comodel_name='payment.transaction', inverse_name='token_id')
verified = fields.Boolean(string="Verified") verified = fields.Boolean(string="Verified")
@@ -33,27 +33,27 @@ class PaymentToken(models.Model):
@api.model_create_multi @api.model_create_multi
def create(self, values_list): def create(self, values_list):
for values in values_list: for values in values_list:
if 'acquirer_id' in values: if 'provider_id' in values:
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id']) provider = self.env['payment.provider'].browse(values['provider_id'])
# Include acquirer-specific create values # Include provider-specific create values
values.update(self._get_specific_create_values(acquirer.provider, values)) values.update(self._get_specific_create_values(provider.code, values))
else: else:
pass # Let psycopg warn about the missing required field pass # Let psycopg warn about the missing required field
return super().create(values_list) return super().create(values_list)
@api.model @api.model
def _get_specific_create_values(self, provider, values): def _get_specific_create_values(self, provider_code, values):
""" Complete the values of the `create` method with acquirer-specific values. """ Complete the values of the `create` method with provider-specific values.
For an acquirer to add its own create values, it must overwrite this method and return a For a provider to add its own create values, it must overwrite this method and return a
dict of values. Acquirer-specific values take precedence over those of the dict of generic dict of values. Provider-specific values take precedence over those of the dict of generic
create values. create values.
:param str provider: The provider of the acquirer managing the token :param str provider_code: The code of the provider managing the token
:param dict values: The original create values :param dict values: The original create values
:return: The dict of acquirer-specific create values :return: The dict of provider-specific create values
:rtype: dict :rtype: dict
""" """
return dict() return dict()
+108 -102
View File
@@ -29,27 +29,27 @@ class PaymentTransaction(models.Model):
def _lang_get(self): def _lang_get(self):
return self.env['res.lang'].get_installed() return self.env['res.lang'].get_installed()
acquirer_id = fields.Many2one( provider_id = fields.Many2one(
string="Acquirer", comodel_name='payment.acquirer', readonly=True, required=True) string="Provider", comodel_name='payment.provider', readonly=True, required=True)
provider = fields.Selection(related='acquirer_id.provider') provider_code = fields.Selection(related='provider_id.code')
company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others) company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others)
related='acquirer_id.company_id', store=True, index=True) related='provider_id.company_id', store=True, index=True)
reference = fields.Char( reference = fields.Char(
string="Reference", help="The internal reference of the transaction", readonly=True, string="Reference", help="The internal reference of the transaction", readonly=True,
required=True) # Already has an index from the UNIQUE SQL constraint required=True) # Already has an index from the UNIQUE SQL constraint
acquirer_reference = fields.Char( provider_reference = fields.Char(
string="Acquirer Reference", help="The acquirer reference of the transaction", string="Provider Reference", help="The provider reference of the transaction",
readonly=True) # This is not the same thing as the acquirer reference of the token readonly=True) # This is not the same thing as the provider reference of the token
amount = fields.Monetary( amount = fields.Monetary(
string="Amount", currency_field='currency_id', readonly=True, required=True) string="Amount", currency_field='currency_id', readonly=True, required=True)
currency_id = fields.Many2one( currency_id = fields.Many2one(
string="Currency", comodel_name='res.currency', readonly=True, required=True) string="Currency", comodel_name='res.currency', readonly=True, required=True)
fees = fields.Monetary( fees = fields.Monetary(
string="Fees", currency_field='currency_id', string="Fees", currency_field='currency_id',
help="The fees amount; set by the system as it depends on the acquirer", readonly=True) help="The fees amount; set by the system as it depends on the provider", readonly=True)
token_id = fields.Many2one( token_id = fields.Many2one(
string="Payment Token", comodel_name='payment.token', readonly=True, string="Payment Token", comodel_name='payment.token', readonly=True,
domain='[("acquirer_id", "=", "acquirer_id")]', ondelete='restrict') domain='[("provider_id", "=", "provider_id")]', ondelete='restrict')
state = fields.Selection( state = fields.Selection(
string="Status", string="Status",
selection=[('draft', "Draft"), ('pending', "Pending"), ('authorized', "Authorized"), selection=[('draft', "Draft"), ('pending', "Pending"), ('authorized', "Authorized"),
@@ -145,12 +145,12 @@ class PaymentTransaction(models.Model):
def _check_state_authorized_supported(self): def _check_state_authorized_supported(self):
""" Check that authorization is supported for a transaction in the 'authorized' state. """ """ Check that authorization is supported for a transaction in the 'authorized' state. """
illegal_authorize_state_txs = self.filtered( illegal_authorize_state_txs = self.filtered(
lambda tx: tx.state == 'authorized' and not tx.acquirer_id.support_manual_capture lambda tx: tx.state == 'authorized' and not tx.provider_id.support_manual_capture
) )
if illegal_authorize_state_txs: if illegal_authorize_state_txs:
raise ValidationError(_( raise ValidationError(_(
"Transaction authorization is not supported by the following payment acquirers: %s", "Transaction authorization is not supported by the following payment providers: %s",
', '.join(set(illegal_authorize_state_txs.mapped('acquirer_id.name'))) ', '.join(set(illegal_authorize_state_txs.mapped('provider_id.name')))
)) ))
@api.constrains('token_id') @api.constrains('token_id')
@@ -164,10 +164,10 @@ class PaymentTransaction(models.Model):
@api.model_create_multi @api.model_create_multi
def create(self, values_list): def create(self, values_list):
for values in values_list: for values in values_list:
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id']) provider = self.env['payment.provider'].browse(values['provider_id'])
if not values.get('reference'): if not values.get('reference'):
values['reference'] = self._compute_reference(acquirer.provider, **values) values['reference'] = self._compute_reference(provider.code, **values)
# Duplicate partner values # Duplicate partner values
partner = self.env['res.partner'].browse(values['partner_id']) partner = self.env['res.partner'].browse(values['partner_id'])
@@ -190,12 +190,12 @@ class PaymentTransaction(models.Model):
values['fees'] = 0 values['fees'] = 0
else: else:
currency = self.env['res.currency'].browse(values.get('currency_id')).exists() currency = self.env['res.currency'].browse(values.get('currency_id')).exists()
values['fees'] = acquirer._compute_fees( values['fees'] = provider._compute_fees(
values.get('amount', 0), currency, partner.country_id, values.get('amount', 0), currency, partner.country_id,
) )
# Include acquirer-specific create values # Include provider-specific create values
values.update(self._get_specific_create_values(acquirer.provider, values)) values.update(self._get_specific_create_values(provider.code, values))
# Generate the hash for the callback if one has be configured on the tx # Generate the hash for the callback if one has be configured on the tx
values['callback_hash'] = self._generate_callback_hash( values['callback_hash'] = self._generate_callback_hash(
@@ -219,16 +219,16 @@ class PaymentTransaction(models.Model):
return txs return txs
@api.model @api.model
def _get_specific_create_values(self, provider, values): def _get_specific_create_values(self, provider_code, values):
""" Complete the values of the `create` method with acquirer-specific values. """ Complete the values of the `create` method with provider-specific values.
For an acquirer to add its own create values, it must overwrite this method and return a For a provider to add its own create values, it must overwrite this method and return a
dict of values. Acquirer-specific values take precedence over those of the dict of generic dict of values. Provider-specific values take precedence over those of the dict of generic
create values. create values.
:param str provider: The provider of the acquirer that handled the transaction :param str provider_code: The code of the provider that handled the transaction
:param dict values: The original create values :param dict values: The original create values
:return: The dict of acquirer-specific create values :return: The dict of provider-specific create values
:rtype: dict :rtype: dict
""" """
return dict() return dict()
@@ -268,7 +268,7 @@ class PaymentTransaction(models.Model):
payment_utils.check_rights_on_recordset(self) payment_utils.check_rights_on_recordset(self)
for tx in self: for tx in self:
# In sudo mode because we need to be able to read on acquirer fields. # In sudo mode because we need to be able to read on provider fields.
tx.sudo()._send_capture_request() tx.sudo()._send_capture_request()
def action_void(self): def action_void(self):
@@ -278,7 +278,7 @@ class PaymentTransaction(models.Model):
payment_utils.check_rights_on_recordset(self) payment_utils.check_rights_on_recordset(self)
for tx in self: for tx in self:
# In sudo mode because we need to be able to read on acquirer fields. # In sudo mode because we need to be able to read on provider fields.
tx.sudo()._send_void_request() tx.sudo()._send_void_request()
def action_refund(self, amount_to_refund=None): def action_refund(self, amount_to_refund=None):
@@ -296,7 +296,7 @@ class PaymentTransaction(models.Model):
#=== BUSINESS METHODS - PAYMENT FLOW ===# #=== BUSINESS METHODS - PAYMENT FLOW ===#
@api.model @api.model
def _compute_reference(self, provider, prefix=None, separator='-', **kwargs): def _compute_reference(self, provider_code, prefix=None, separator='-', **kwargs):
""" Compute a unique reference for the transaction. """ Compute a unique reference for the transaction.
The reference either corresponds to the prefix if no other transaction with that prefix The reference either corresponds to the prefix if no other transaction with that prefix
@@ -319,7 +319,7 @@ class PaymentTransaction(models.Model):
the full reference will be 'INV1-INV2' (or similar) if no existing reference has the the full reference will be 'INV1-INV2' (or similar) if no existing reference has the
same prefix, or 'INV1-INV2-n' if n existing references have the same prefix. same prefix, or 'INV1-INV2-n' if n existing references have the same prefix.
:param str provider: The provider of the acquirer handling the transaction :param str provider_code: The code of the provider handling the transaction
:param str prefix: The custom prefix used to compute the full reference :param str prefix: The custom prefix used to compute the full reference
:param str separator: The custom separator used to separate the prefix from the suffix, and :param str separator: The custom separator used to separate the prefix from the suffix, and
passed to `_compute_reference_prefix` if it is called passed to `_compute_reference_prefix` if it is called
@@ -333,7 +333,7 @@ class PaymentTransaction(models.Model):
# Replace special characters by their ASCII alternative (é -> e ; ä -> a ; ...) # Replace special characters by their ASCII alternative (é -> e ; ä -> a ; ...)
prefix = unicodedata.normalize('NFKD', prefix).encode('ascii', 'ignore').decode('utf-8') prefix = unicodedata.normalize('NFKD', prefix).encode('ascii', 'ignore').decode('utf-8')
if not prefix: # Prefix not provided or voided above, compute it based on the kwargs if not prefix: # Prefix not provided or voided above, compute it based on the kwargs
prefix = self.sudo()._compute_reference_prefix(provider, separator, **kwargs) prefix = self.sudo()._compute_reference_prefix(provider_code, separator, **kwargs)
if not prefix: # Prefix not computed from the kwargs, fallback on time-based value if not prefix: # Prefix not computed from the kwargs, fallback on time-based value
prefix = payment_utils.singularize_reference_prefix() prefix = payment_utils.singularize_reference_prefix()
@@ -372,12 +372,12 @@ class PaymentTransaction(models.Model):
return reference return reference
@api.model @api.model
def _compute_reference_prefix(self, provider, separator, **values): def _compute_reference_prefix(self, provider_code, separator, **values):
""" Compute the reference prefix from the transaction values. """ Compute the reference prefix from the transaction values.
Note: This method should be called in sudo mode to give access to documents (INV, SO, ...). Note: This method should be called in sudo mode to give access to documents (INV, SO, ...).
:param str provider: The provider of the acquirer handling the transaction :param str provider_code: The code of the provider handling the transaction
:param str separator: The custom separator used to separate data references :param str separator: The custom separator used to separate data references
:param dict values: The transaction values used to compute the reference prefix. :param dict values: The transaction values used to compute the reference prefix.
:return: an empty string :return: an empty string
@@ -409,13 +409,13 @@ class PaymentTransaction(models.Model):
The returned dict contains the following entries: The returned dict contains the following entries:
- tx_id: The transaction, as a `payment.transaction` id - tx_id: The transaction, as a `payment.transaction` id
- acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id - provider_id: The provider handling the transaction, as a `payment.provider` id
- provider: The provider of the acquirer - provider_code: The code of the provider
- reference: The reference of the transaction - reference: The reference of the transaction
- amount: The rounded amount of the transaction - amount: The rounded amount of the transaction
- currency_id: The currency of the transaction, as a res.currency id - currency_id: The currency of the transaction, as a res.currency id
- partner_id: The partner making the transaction, as a res.partner id - partner_id: The partner making the transaction, as a res.partner id
- Additional acquirer-specific entries - Additional provider-specific entries
Note: self.ensure_one() Note: self.ensure_one()
@@ -426,31 +426,31 @@ class PaymentTransaction(models.Model):
processing_values = { processing_values = {
'tx_id': self.id, 'tx_id': self.id,
'acquirer_id': self.acquirer_id.id, 'provider_id': self.provider_id.id,
'provider': self.provider, 'provider_code': self.provider_code,
'reference': self.reference, 'reference': self.reference,
'amount': self.amount, 'amount': self.amount,
'currency_id': self.currency_id.id, 'currency_id': self.currency_id.id,
'partner_id': self.partner_id.id, 'partner_id': self.partner_id.id,
} }
# Complete generic processing values with acquirer-specific values # Complete generic processing values with provider-specific values
processing_values.update(self._get_specific_processing_values(processing_values)) processing_values.update(self._get_specific_processing_values(processing_values))
_logger.info( _logger.info(
"generic and acquirer-specific processing values for transaction with reference " "generic and provider-specific processing values for transaction with reference "
"%(ref)s:\n%(values)s", "%(ref)s:\n%(values)s",
{'ref': self.reference, 'values': pprint.pformat(processing_values)}, {'ref': self.reference, 'values': pprint.pformat(processing_values)},
) )
# Render the html form for the redirect flow if available # Render the html form for the redirect flow if available
if self.operation in ('online_redirect', 'validation'): if self.operation in ('online_redirect', 'validation'):
redirect_form_view = self.acquirer_id._get_redirect_form_view( redirect_form_view = self.provider_id._get_redirect_form_view(
is_validation=self.operation == 'validation' is_validation=self.operation == 'validation'
) )
if redirect_form_view: # Some acquirer don't need a redirect form if redirect_form_view: # Some provider don't need a redirect form
rendering_values = self._get_specific_rendering_values(processing_values) rendering_values = self._get_specific_rendering_values(processing_values)
_logger.info( _logger.info(
"acquirer-specific rendering values for transaction with reference " "provider-specific rendering values for transaction with reference "
"%(ref)s:\n%(values)s", "%(ref)s:\n%(values)s",
{'ref': self.reference, 'values': pprint.pformat(rendering_values)}, {'ref': self.reference, 'values': pprint.pformat(rendering_values)},
) )
@@ -460,36 +460,36 @@ class PaymentTransaction(models.Model):
return processing_values return processing_values
def _get_specific_processing_values(self, processing_values): def _get_specific_processing_values(self, processing_values):
""" Return a dict of acquirer-specific values used to process the transaction. """ Return a dict of provider-specific values used to process the transaction.
For an acquirer to add its own processing values, it must overwrite this method and return a For a provider to add its own processing values, it must overwrite this method and return a
dict of acquirer-specific values based on the generic values returned by this method. dict of provider-specific values based on the generic values returned by this method.
Acquirer-specific values take precedence over those of the dict of generic processing Provider-specific values take precedence over those of the dict of generic processing
values. values.
:param dict processing_values: The generic processing values of the transaction :param dict processing_values: The generic processing values of the transaction
:return: The dict of acquirer-specific processing values :return: The dict of provider-specific processing values
:rtype: dict :rtype: dict
""" """
return dict() return dict()
def _get_specific_rendering_values(self, processing_values): def _get_specific_rendering_values(self, processing_values):
""" Return a dict of acquirer-specific values used to render the redirect form. """ Return a dict of provider-specific values used to render the redirect form.
For an acquirer to add its own rendering values, it must overwrite this method and return a For a provider to add its own rendering values, it must overwrite this method and return a
dict of acquirer-specific values based on the processing values (acquirer-specific dict of provider-specific values based on the processing values (provider-specific
processing values included). processing values included).
:param dict processing_values: The processing values of the transaction :param dict processing_values: The processing values of the transaction
:return: The dict of acquirer-specific rendering values :return: The dict of provider-specific rendering values
:rtype: dict :rtype: dict
""" """
return dict() return dict()
def _send_payment_request(self): def _send_payment_request(self):
""" Request the provider of the acquirer handling the transaction to execute the payment. """ Request the provider handling the transaction to execute the payment.
For an acquirer to support tokenization, it must override this method and call it to log the For a provider to support tokenization, it must override this method and call it to log the
'sent' message, then request a money transfer to its provider. 'sent' message, then request a money transfer to its provider.
Note: self.ensure_one() Note: self.ensure_one()
@@ -497,13 +497,13 @@ class PaymentTransaction(models.Model):
:return: None :return: None
""" """
self.ensure_one() self.ensure_one()
self._ensure_acquirer_is_not_disabled() self._ensure_provider_is_not_disabled()
self._log_sent_message() self._log_sent_message()
def _send_refund_request(self, amount_to_refund=None, create_refund_transaction=True): def _send_refund_request(self, amount_to_refund=None, create_refund_transaction=True):
""" Request the provider of the acquirer handling the transaction to refund it. """ Request the provider handling the transaction to refund it.
For an acquirer to support refunds, it must override this method and request a refund For a provider to support refunds, it must override this method and request a refund
to its provider. to its provider.
Note: self.ensure_one() Note: self.ensure_one()
@@ -514,7 +514,7 @@ class PaymentTransaction(models.Model):
:rtype: recordset of `payment.transaction` :rtype: recordset of `payment.transaction`
""" """
self.ensure_one() self.ensure_one()
self._ensure_acquirer_is_not_disabled() self._ensure_provider_is_not_disabled()
if create_refund_transaction: if create_refund_transaction:
refund_tx = self._create_refund_transaction(amount_to_refund=amount_to_refund) refund_tx = self._create_refund_transaction(amount_to_refund=amount_to_refund)
@@ -534,8 +534,8 @@ class PaymentTransaction(models.Model):
self.ensure_one() self.ensure_one()
return self.create({ return self.create({
'acquirer_id': self.acquirer_id.id, 'provider_id': self.provider_id.id,
'reference': self._compute_reference(self.provider, prefix=f'R-{self.reference}'), 'reference': self._compute_reference(self.provider_code, prefix=f'R-{self.reference}'),
'amount': -(amount_to_refund or self.amount), 'amount': -(amount_to_refund or self.amount),
'currency_id': self.currency_id.id, 'currency_id': self.currency_id.id,
'token_id': self.token_id.id, 'token_id': self.token_id.id,
@@ -546,9 +546,9 @@ class PaymentTransaction(models.Model):
}) })
def _send_capture_request(self): def _send_capture_request(self):
""" Request the provider of the acquirer handling the transaction to capture it. """ Request the provider handling the transaction to capture it.
For an acquirer to support authorization, it must override this method and request a capture For a provider to support authorization, it must override this method and request a capture
to its provider. to its provider.
Note: self.ensure_one() Note: self.ensure_one()
@@ -556,12 +556,12 @@ class PaymentTransaction(models.Model):
:return: None :return: None
""" """
self.ensure_one() self.ensure_one()
self._ensure_acquirer_is_not_disabled() self._ensure_provider_is_not_disabled()
def _send_void_request(self): def _send_void_request(self):
""" Request the provider of the acquirer handling the transaction to void it. """ Request the provider handling the transaction to void it.
For an acquirer to support authorization, it must override this method and request the For a provider to support authorization, it must override this method and request the
transaction to be voided to its provider. transaction to be voided to its provider.
Note: self.ensure_one() Note: self.ensure_one()
@@ -569,40 +569,40 @@ class PaymentTransaction(models.Model):
:return: None :return: None
""" """
self.ensure_one() self.ensure_one()
self._ensure_acquirer_is_not_disabled() self._ensure_provider_is_not_disabled()
def _ensure_acquirer_is_not_disabled(self): def _ensure_provider_is_not_disabled(self):
""" Ensure that the acquirer's state is not 'disabled' before sending a request to its """ Ensure that the provider's state is not 'disabled' before sending a request to its
provider. provider.
:return: None :return: None
:raise UserError: If the acquirer's state is 'disabled'. :raise UserError: If the provider's state is 'disabled'.
""" """
if self.acquirer_id.state == 'disabled': if self.provider_id.state == 'disabled':
raise UserError(_( raise UserError(_(
"Making a request to the provider is not possible because the acquirer is disabled." "Making a request to the provider is not possible because the provider is disabled."
)) ))
def _handle_notification_data(self, provider, notification_data): def _handle_notification_data(self, provider_code, notification_data):
""" Match the transaction with the notification data, update its state and return it. """ Match the transaction with the notification data, update its state and return it.
:param str provider: The provider of the acquirer that handled the transaction :param str provider_code: The code of the provider that handled the transaction
:param dict notification_data: The notification data sent by the provider :param dict notification_data: The notification data sent by the provider
:return: The transaction :return: The transaction
:rtype: recordset of `payment.transaction` :rtype: recordset of `payment.transaction`
""" """
tx = self._get_tx_from_notification_data(provider, notification_data) tx = self._get_tx_from_notification_data(provider_code, notification_data)
tx._process_notification_data(notification_data) tx._process_notification_data(notification_data)
tx._execute_callback() tx._execute_callback()
return tx return tx
def _get_tx_from_notification_data(self, provider, notification_data): def _get_tx_from_notification_data(self, provider_code, notification_data):
""" Find the transaction based on the notification data. """ Find the transaction based on the notification data.
For an acquirer to handle transaction processing, it must overwrite this method and return For a provider to handle transaction processing, it must overwrite this method and return
the transaction matching the notification data. the transaction matching the notification data.
:param str provider: The provider of the acquirer that handled the transaction :param str provider_code: The code of the provider that handled the transaction
:param dict notification_data: The notification data sent by the provider :param dict notification_data: The notification data sent by the provider
:return: The transaction if found :return: The transaction if found
:rtype: recordset of `payment.transaction` :rtype: recordset of `payment.transaction`
@@ -610,12 +610,12 @@ class PaymentTransaction(models.Model):
return self return self
def _process_notification_data(self, notification_data): def _process_notification_data(self, notification_data):
""" Update the transaction state and the acquirer reference based on the notification data. """ Update the transaction state and the provider reference based on the notification data.
This method should normally never be called directly. The correct method to call upon This method should normally never be called directly. The correct method to call upon
receiving notification data is `_handle_notification_data`. receiving notification data is `_handle_notification_data`.
For an acquirer to handle transaction processing, it must overwrite this method and process For a provider to handle transaction processing, it must overwrite this method and process
the notification data. the notification data.
Note: self.ensure_one() Note: self.ensure_one()
@@ -799,12 +799,12 @@ class PaymentTransaction(models.Model):
def _get_post_processing_values(self): def _get_post_processing_values(self):
""" Return a dict of values used to display the status of the transaction. """ Return a dict of values used to display the status of the transaction.
For an acquirer to handle transaction status display, it must override this method and For a provider to handle transaction status display, it must override this method and
return a dict of values. Acquirer-specific values take precedence over those of the dict of return a dict of values. Provider-specific values take precedence over those of the dict of
generic post-processing values. generic post-processing values.
The returned dict contains the following entries: The returned dict contains the following entries:
- provider: The provider of the acquirer - provider_code: The code of the provider
- reference: The reference of the transaction - reference: The reference of the transaction
- amount: The rounded amount of the transaction - amount: The rounded amount of the transaction
- currency_id: The currency of the transaction, as a res.currency id - currency_id: The currency of the transaction, as a res.currency id
@@ -812,7 +812,7 @@ class PaymentTransaction(models.Model):
- state_message: The information message about the state - state_message: The information message about the state
- is_post_processed: Whether the transaction has already been post-processed - is_post_processed: Whether the transaction has already been post-processed
- landing_route: The route the user is redirected to after the transaction - landing_route: The route the user is redirected to after the transaction
- Additional acquirer-specific entries - Additional provider-specific entries
Note: self.ensure_one() Note: self.ensure_one()
@@ -822,7 +822,7 @@ class PaymentTransaction(models.Model):
self.ensure_one() self.ensure_one()
post_processing_values = { post_processing_values = {
'provider': self.provider, 'provider_code': self.provider_code,
'reference': self.reference, 'reference': self.reference,
'amount': self.amount, 'amount': self.amount,
'currency_code': self.currency_id.name, 'currency_code': self.currency_id.name,
@@ -832,8 +832,8 @@ class PaymentTransaction(models.Model):
'landing_route': self.landing_route, 'landing_route': self.landing_route,
} }
_logger.debug( _logger.debug(
"post-processing values of transaction with reference %s for acquirer with id %s:\n%s", "post-processing values of transaction with reference %s for provider with id %s:\n%s",
self.reference, self.acquirer_id.id, pprint.pformat(post_processing_values) self.reference, self.provider_id.id, pprint.pformat(post_processing_values)
) # DEBUG level because this can get spammy with transactions in non-final states ) # DEBUG level because this can get spammy with transactions in non-final states
return post_processing_values return post_processing_values
@@ -904,7 +904,7 @@ class PaymentTransaction(models.Model):
def _log_received_message(self): def _log_received_message(self):
""" Log in the chatter of relevant documents that the transactions have been received. """ Log in the chatter of relevant documents that the transactions have been received.
A transaction is 'received' when a response is received from the provider of the acquirer A transaction is 'received' when a response is received from the system of the provider
handling the transaction. handling the transaction.
:return: None :return: None
@@ -941,30 +941,30 @@ class PaymentTransaction(models.Model):
# Choose the message based on the payment flow # Choose the message based on the payment flow
if self.operation in ('online_redirect', 'online_direct'): if self.operation in ('online_redirect', 'online_direct'):
message = _( message = _(
"A transaction with reference %(ref)s has been initiated (%(acq_name)s).", "A transaction with reference %(ref)s has been initiated (%(provider_name)s).",
ref=self.reference, acq_name=self.acquirer_id.name ref=self.reference, provider_name=self.provider_id.name
) )
elif self.operation == 'refund': elif self.operation == 'refund':
formatted_amount = format_amount(self.env, -self.amount, self.currency_id) formatted_amount = format_amount(self.env, -self.amount, self.currency_id)
message = _( message = _(
"A refund request of %(amount)s has been sent. The payment will be created soon. " "A refund request of %(amount)s has been sent. The payment will be created soon. "
"Refund transaction reference: %(ref)s (%(acq_name)s).", "Refund transaction reference: %(ref)s (%(provider_name)s).",
amount=formatted_amount, ref=self.reference, acq_name=self.acquirer_id.name amount=formatted_amount, ref=self.reference, provider_name=self.provider_id.name
) )
elif self.operation in ('online_token', 'offline'): elif self.operation in ('online_token', 'offline'):
message = _( message = _(
"A transaction with reference %(ref)s has been initiated using the payment method " "A transaction with reference %(ref)s has been initiated using the payment method "
"%(token)s (%(acq_name)s).", "%(token)s (%(provider_name)s).",
ref=self.reference, ref=self.reference,
token=self.token_id._build_display_name(), token=self.token_id._build_display_name(),
acq_name=self.acquirer_id.name provider_name=self.provider_id.name
) )
else: # 'validation' else: # 'validation'
message = _( message = _(
"A transaction with reference %(ref)s has been initiated to save a new payment " "A transaction with reference %(ref)s has been initiated to save a new payment "
"method (%(acq_name)s)", "method (%(provider_name)s)",
ref=self.reference, ref=self.reference,
acq_name=self.acquirer_id.name, provider_name=self.provider_id.name,
) )
return message return message
@@ -978,33 +978,39 @@ class PaymentTransaction(models.Model):
formatted_amount = format_amount(self.env, self.amount, self.currency_id) formatted_amount = format_amount(self.env, self.amount, self.currency_id)
if self.state == 'pending': if self.state == 'pending':
message = _( message = _(
"The transaction with reference %(ref)s for %(amount)s is pending (%(acq_name)s).", ("The transaction with reference %(ref)s for %(amount)s "
ref=self.reference, amount=formatted_amount, acq_name=self.acquirer_id.name "is pending (%(provider_name)s)."),
ref=self.reference,
amount=formatted_amount,
provider_name=self.provider_id.name
) )
elif self.state == 'authorized': elif self.state == 'authorized':
message = _( message = _(
"The transaction with reference %(ref)s for %(amount)s has been authorized " "The transaction with reference %(ref)s for %(amount)s has been authorized "
"(%(acq_name)s).", ref=self.reference, amount=formatted_amount, "(%(provider_name)s).", ref=self.reference, amount=formatted_amount,
acq_name=self.acquirer_id.name provider_name=self.provider_id.name
) )
elif self.state == 'done': elif self.state == 'done':
message = _( message = _(
"The transaction with reference %(ref)s for %(amount)s has been confirmed " "The transaction with reference %(ref)s for %(amount)s has been confirmed "
"(%(acq_name)s).", ref=self.reference, amount=formatted_amount, "(%(provider_name)s).", ref=self.reference, amount=formatted_amount,
acq_name=self.acquirer_id.name provider_name=self.provider_id.name
) )
elif self.state == 'error': elif self.state == 'error':
message = _( message = _(
"The transaction with reference %(ref)s for %(amount)s encountered an error" "The transaction with reference %(ref)s for %(amount)s encountered an error"
" (%(acq_name)s).", " (%(provider_name)s).",
ref=self.reference, amount=formatted_amount, acq_name=self.acquirer_id.name ref=self.reference, amount=formatted_amount, provider_name=self.provider_id.name
) )
if self.state_message: if self.state_message:
message += "<br />" + _("Error: %s", self.state_message) message += "<br />" + _("Error: %s", self.state_message)
else: else:
message = _( message = _(
"The transaction with reference %(ref)s for %(amount)s is canceled (%(acq_name)s).", ("The transaction with reference %(ref)s for %(amount)s is canceled "
ref=self.reference, amount=formatted_amount, acq_name=self.acquirer_id.name "(%(provider_name)s)."),
ref=self.reference,
amount=formatted_amount,
provider_name=self.provider_id.name
) )
if self.state_message: if self.state_message:
message += "<br />" + _("Reason: %s", self.state_message) message += "<br />" + _("Reason: %s", self.state_message)
+15 -15
View File
@@ -6,8 +6,8 @@ from odoo import api, fields, models
class ResCompany(models.Model): class ResCompany(models.Model):
_inherit = 'res.company' _inherit = 'res.company'
payment_acquirer_onboarding_state = fields.Selection( payment_provider_onboarding_state = fields.Selection(
string="State of the onboarding payment acquirer step", string="State of the onboarding payment provider step",
selection=[('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done")], selection=[('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done")],
default='not_done') default='not_done')
payment_onboarding_payment_method = fields.Selection( payment_onboarding_payment_method = fields.Selection(
@@ -20,7 +20,7 @@ class ResCompany(models.Model):
]) ])
def _run_payment_onboarding_step(self, menu_id): def _run_payment_onboarding_step(self, menu_id):
""" Install the suggested payment modules and configure the acquirers. """ Install the suggested payment modules and configure the providers.
It's checked that the current company has a Chart of Account. It's checked that the current company has a Chart of Account.
@@ -41,18 +41,18 @@ class ResCompany(models.Model):
) )
# Configure Stripe # Configure Stripe
stripe_acquirer = new_env.ref('payment.payment_acquirer_stripe') stripe_provider = new_env.ref('payment.payment_provider_stripe')
stripe_acquirer.journal_id = stripe_acquirer.journal_id or default_journal stripe_provider.journal_id = stripe_provider.journal_id or default_journal
if stripe_acquirer.state == 'disabled': # The onboarding step has never been run if stripe_provider.state == 'disabled': # The onboarding step has never been run
# Configure PayPal # Configure PayPal
paypal_acquirer = new_env.ref('payment.payment_acquirer_paypal') paypal_provider = new_env.ref('payment.payment_provider_paypal')
if not paypal_acquirer.paypal_email_account: if not paypal_provider.paypal_email_account:
paypal_acquirer.paypal_email_account = new_env.user.email or new_env.company.email paypal_provider.paypal_email_account = new_env.user.email or new_env.company.email
if paypal_acquirer.state == 'disabled' and paypal_acquirer.paypal_email_account: if paypal_provider.state == 'disabled' and paypal_provider.paypal_email_account:
paypal_acquirer.state = 'enabled' paypal_provider.state = 'enabled'
paypal_acquirer.journal_id = paypal_acquirer.journal_id or default_journal paypal_provider.journal_id = paypal_provider.journal_id or default_journal
return stripe_acquirer.action_stripe_connect_account(menu_id=menu_id) return stripe_provider.action_stripe_connect_account(menu_id=menu_id)
def _install_modules(self, module_names): def _install_modules(self, module_names):
modules_sudo = self.env['ir.module.module'].sudo().search([('name', 'in', module_names)]) modules_sudo = self.env['ir.module.module'].sudo().search([('name', 'in', module_names)])
@@ -64,9 +64,9 @@ class ResCompany(models.Model):
:return: None :return: None
""" """
self.set_onboarding_step_done('payment_acquirer_onboarding_state') self.set_onboarding_step_done('payment_provider_onboarding_state')
def get_account_invoice_onboarding_steps_states_names(self): def get_account_invoice_onboarding_steps_states_names(self):
""" Override of account. """ """ Override of account. """
steps = super().get_account_invoice_onboarding_steps_states_names() steps = super().get_account_invoice_onboarding_steps_states_names()
return steps + ['payment_acquirer_onboarding_state'] return steps + ['payment_provider_onboarding_state']
+2 -2
View File
@@ -1,7 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_payment_link_wizard,access_payment_link_wizard,payment.model_payment_link_wizard,base.group_user,0,0,0,0 access_payment_link_wizard,access_payment_link_wizard,payment.model_payment_link_wizard,base.group_user,0,0,0,0
payment_acquirer_onboarding_wizard,payment.acquirer.onboarding.wizard,model_payment_acquirer_onboarding_wizard,base.group_system,1,1,1,0 payment_provider_onboarding_wizard,payment.provider.onboarding.wizard,model_payment_provider_onboarding_wizard,base.group_system,1,1,1,0
payment_acquirer_system,payment.acquirer.system,model_payment_acquirer,base.group_system,1,1,1,1 payment_provider_system,payment.provider.system,model_payment_provider,base.group_system,1,1,1,1
payment_icon_all,payment.icon.all,model_payment_icon,,1,0,0,0 payment_icon_all,payment.icon.all,model_payment_icon,,1,0,0,0
payment_icon_system,payment.icon.system,model_payment_icon,base.group_system,1,1,1,1 payment_icon_system,payment.icon.system,model_payment_icon,base.group_system,1,1,1,1
payment_token_all,payment.token.all,model_payment_token,,1,0,0,0 payment_token_all,payment.token.all,model_payment_token,,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_payment_link_wizard access_payment_link_wizard payment.model_payment_link_wizard base.group_user 0 0 0 0
3 payment_acquirer_onboarding_wizard payment_provider_onboarding_wizard payment.acquirer.onboarding.wizard payment.provider.onboarding.wizard model_payment_acquirer_onboarding_wizard model_payment_provider_onboarding_wizard base.group_system 1 1 1 0
4 payment_acquirer_system payment_provider_system payment.acquirer.system payment.provider.system model_payment_acquirer model_payment_provider base.group_system 1 1 1 1
5 payment_icon_all payment.icon.all model_payment_icon 1 0 0 0
6 payment_icon_system payment.icon.system model_payment_icon base.group_system 1 1 1 1
7 payment_token_all payment.token.all model_payment_token 1 0 0 0
+4 -4
View File
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1"> <odoo noupdate="1">
<!-- Acquirers --> <!-- Providers -->
<record id="payment_acquirer_company_rule" model="ir.rule"> <record id="payment_provider_company_rule" model="ir.rule">
<field name="name">Access acquirers in own companies only</field> <field name="name">Access providers in own companies only</field>
<field name="model_id" ref="payment.model_payment_acquirer"/> <field name="model_id" ref="payment.model_payment_provider"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field> <field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record> </record>
@@ -63,7 +63,7 @@ odoo.define('payment.checkout_form', require => {
// Update the tx context with the value of the "Save my payment details" checkbox // Update the tx context with the value of the "Save my payment details" checkbox
if (flow !== 'token') { if (flow !== 'token') {
const $tokenizeCheckbox = this.$( const $tokenizeCheckbox = this.$(
`#o_payment_acquirer_inline_form_${paymentOptionId}` // Only match acq. radios `#o_payment_provider_inline_form_${paymentOptionId}` // Only match provider radios
).find('input[name="o_payment_save_as_token"]'); ).find('input[name="o_payment_save_as_token"]');
this.txContext.tokenizationRequested = $tokenizeCheckbox.length === 1 this.txContext.tokenizationRequested = $tokenizeCheckbox.length === 1
&& $tokenizeCheckbox[0].checked; && $tokenizeCheckbox[0].checked;
@@ -48,31 +48,31 @@ publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({
_isShippingInformationRequired: () => false, _isShippingInformationRequired: () => false,
/** /**
* Prepare the acquirer-specific express checkout form based on the provided data. * Prepare the provider-specific express checkout form based on the provided data.
* *
* For an acquirer to manage an express checkout form, it must override this method. * For an provider to manage an express checkout form, it must override this method.
* *
* @private * @private
* @param {Object} acquirerData - The acquirer-specific data. * @param {Object} providerData - The provider-specific data.
* @return {Promise} * @return {Promise}
*/ */
async _prepareExpressCheckoutForm(acquirerData) { async _prepareExpressCheckoutForm(providerData) {
return Promise.resolve(); return Promise.resolve();
}, },
/** /**
* Prepare the params to send to the transaction route. * Prepare the params to send to the transaction route.
* *
* For an acquirer to overwrite generic params or to add acquirer-specific ones, it must * For an provider to overwrite generic params or to add provider-specific ones, it must
* override this method and return the extended transaction route params. * override this method and return the extended transaction route params.
* *
* @private * @private
* @param {number} acquirerId - The id of the acquirer handling the transaction. * @param {number} providerId - The id of the provider handling the transaction.
* @returns {object} - The transaction route params * @returns {object} - The transaction route params
*/ */
_prepareTransactionRouteParams(acquirerId) { _prepareTransactionRouteParams(providerId) {
return { return {
'payment_option_id': parseInt(acquirerId), 'payment_option_id': parseInt(providerId),
'reference_prefix': this.txContext.referencePrefix && 'reference_prefix': this.txContext.referencePrefix &&
this.txContent.referencePrefix.toString(), this.txContent.referencePrefix.toString(),
'currency_id': this.txContext.currencyId && 'currency_id': this.txContext.currencyId &&
@@ -107,15 +107,15 @@ publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({
/** /**
* Update the amount of the express checkout form. * Update the amount of the express checkout form.
* *
* For an acquirer to manage an express form, it must override this method. * For a provider to manage an express form, it must override this method.
* *
* @private * @private
* @param {Object} acquirerData - The acquirer-specific data. * @param {Object} providerData - The provider-specific data.
* @param {number} newAmount - The new amount. * @param {number} newAmount - The new amount.
* @param {number} newMinorAmount - The new minor amount. * @param {number} newMinorAmount - The new minor amount.
* @return {undefined} * @return {undefined}
*/ */
_updateAmount(acquirerData, newAmount, newMinorAmount) {}, _updateAmount(providerData, newAmount, newMinorAmount) {},
}); });
@@ -27,7 +27,7 @@ odoo.define('payment.payment_form_mixin', require => {
this._displayInlineForm(checkedRadio); this._displayInlineForm(checkedRadio);
this._enableButton(); this._enableButton();
} else { } else {
this._setPaymentFlow(); // Initialize the payment flow to let acquirers overwrite it this._setPaymentFlow(); // Initialize the payment flow to let providers overwrite it
} }
}, },
@@ -117,7 +117,7 @@ odoo.define('payment.payment_form_mixin', require => {
_displayInlineForm: function (radio) { _displayInlineForm: function (radio) {
this._hideInlineForms(); // Collapse previously opened inline forms this._hideInlineForms(); // Collapse previously opened inline forms
this._hideError(); // The error is only relevant until it is hidden with its inline form this._hideError(); // The error is only relevant until it is hidden with its inline form
this._setPaymentFlow(); // Reset the payment flow to let acquirers overwrite it this._setPaymentFlow(); // Reset the payment flow to let providers overwrite it
// Extract contextual values from the radio button // Extract contextual values from the radio button
const provider = this._getProviderFromRadio(radio); const provider = this._getProviderFromRadio(radio);
@@ -181,7 +181,7 @@ odoo.define('payment.payment_form_mixin', require => {
/** /**
* Determine and return the online payment flow of the selected payment option. * Determine and return the online payment flow of the selected payment option.
* *
* As some acquirers implement both the direct payment and the payment with redirection, the * As some providers implement both the direct payment and the payment with redirection, the
* flow cannot be inferred from the radio button only. The radio button only indicates * flow cannot be inferred from the radio button only. The radio button only indicates
* whether the payment option is a token. If not, the transaction context is looked up to * whether the payment option is a token. If not, the transaction context is looked up to
* determine whether the flow is 'direct' or 'redirect'. * determine whether the flow is 'direct' or 'redirect'.
@@ -208,7 +208,7 @@ odoo.define('payment.payment_form_mixin', require => {
* *
* @private * @private
* @param {HTMLInputElement} radio - The radio button linked to the payment option * @param {HTMLInputElement} radio - The radio button linked to the payment option
* @return {number} The acquirer id or the token id or of the payment option linked to the * @return {number} The provider id or the token id or of the payment option linked to the
* radio button. * radio button.
*/ */
_getPaymentOptionIdFromRadio: radio => $(radio).data('payment-option-id'), _getPaymentOptionIdFromRadio: radio => $(radio).data('payment-option-id'),
@@ -223,7 +223,7 @@ odoo.define('payment.payment_form_mixin', require => {
_getProviderFromRadio: radio => $(radio).data('provider'), _getProviderFromRadio: radio => $(radio).data('provider'),
/** /**
* Remove the error in the acquirer form. * Remove the error in the provider form.
* *
* @private * @private
* @return {jQuery} The removed error * @return {jQuery} The removed error
@@ -280,16 +280,16 @@ odoo.define('payment.payment_form_mixin', require => {
/** /**
* Prepare the params to send to the transaction route. * Prepare the params to send to the transaction route.
* *
* For an acquirer to overwrite generic params or to add acquirer-specific ones, it must * For a provider to overwrite generic params or to add provider-specific ones, it must
* override this method and return the extended transaction route params. * override this method and return the extended transaction route params.
* *
* @private * @private
* @param {string} provider - The provider of the selected payment option's acquirer * @param {string} code - The code of the selected payment option provider
* @param {number} paymentOptionId - The id of the selected payment option * @param {number} paymentOptionId - The id of the selected payment option
* @param {string} flow - The online payment flow of the selected payment option * @param {string} flow - The online payment flow of the selected payment option
* @return {object} The transaction route params * @return {object} The transaction route params
*/ */
_prepareTransactionRouteParams: function (provider, paymentOptionId, flow) { _prepareTransactionRouteParams: function (code, paymentOptionId, flow) {
return { return {
'payment_option_id': paymentOptionId, 'payment_option_id': paymentOptionId,
'reference_prefix': this.txContext.referencePrefix !== undefined 'reference_prefix': this.txContext.referencePrefix !== undefined
@@ -310,49 +310,49 @@ odoo.define('payment.payment_form_mixin', require => {
}, },
/** /**
* Prepare the acquirer-specific inline form of the selected payment option. * Prepare the provider-specific inline form of the selected payment option.
* *
* For an acquirer to manage an inline form, it must override this method. When the override * For a provider to manage an inline form, it must override this method. When the override
* is called, it must lookup the parameters to decide whether it is necessary to prepare its * is called, it must lookup the parameters to decide whether it is necessary to prepare its
* inline form. Otherwise, the call must be sent back to the parent method. * inline form. Otherwise, the call must be sent back to the parent method.
* *
* @private * @private
* @param {string} provider - The provider of the selected payment option's acquirer * @param {string} code - The code of the selected payment option's provider
* @param {number} paymentOptionId - The id of the selected payment option * @param {number} paymentOptionId - The id of the selected payment option
* @param {string} flow - The online payment flow of the selected payment option * @param {string} flow - The online payment flow of the selected payment option
* @return {Promise} * @return {Promise}
*/ */
_prepareInlineForm: (provider, paymentOptionId, flow) => Promise.resolve(), _prepareInlineForm: (code, paymentOptionId, flow) => Promise.resolve(),
/** /**
* Process the payment. * Process the payment.
* *
* For an acquirer to do pre-processing work on the transaction processing flow, or to * For a provider to do pre-processing work on the transaction processing flow, or to
* define its entire own flow that requires re-scheduling the RPC to the transaction route, * define its entire own flow that requires re-scheduling the RPC to the transaction route,
* it must override this method. * it must override this method.
* If only post-processing work is needed, an override of `_processRedirectPayment`, * If only post-processing work is needed, an override of `_processRedirectPayment`,
* `_processDirectPayment` or `_processTokenPayment` might be more appropriate. * `_processDirectPayment` or `_processTokenPayment` might be more appropriate.
* *
* @private * @private
* @param {string} provider - The provider of the payment option's acquirer * @param {string} code - The code of the payment option's provider
* @param {number} paymentOptionId - The id of the payment option handling the transaction * @param {number} paymentOptionId - The id of the payment option handling the transaction
* @param {string} flow - The online payment flow of the transaction * @param {string} flow - The online payment flow of the transaction
* @return {Promise} * @return {Promise}
*/ */
_processPayment: function (provider, paymentOptionId, flow) { _processPayment: function (code, paymentOptionId, flow) {
// Call the transaction route to create a tx and retrieve the processing values // Call the transaction route to create a tx and retrieve the processing values
return this._rpc({ return this._rpc({
route: this.txContext.transactionRoute, route: this.txContext.transactionRoute,
params: this._prepareTransactionRouteParams(provider, paymentOptionId, flow), params: this._prepareTransactionRouteParams(code, paymentOptionId, flow),
}).then(processingValues => { }).then(processingValues => {
if (flow === 'redirect') { if (flow === 'redirect') {
return this._processRedirectPayment( return this._processRedirectPayment(
provider, paymentOptionId, processingValues code, paymentOptionId, processingValues
); );
} else if (flow === 'direct') { } else if (flow === 'direct') {
return this._processDirectPayment(provider, paymentOptionId, processingValues); return this._processDirectPayment(code, paymentOptionId, processingValues);
} else if (flow === 'token') { } else if (flow === 'token') {
return this._processTokenPayment(provider, paymentOptionId, processingValues); return this._processTokenPayment(code, paymentOptionId, processingValues);
} }
}).guardedCatch(error => { }).guardedCatch(error => {
error.event.preventDefault(); error.event.preventDefault();
@@ -365,32 +365,32 @@ odoo.define('payment.payment_form_mixin', require => {
}, },
/** /**
* Execute the acquirer-specific implementation of the direct payment flow. * Execute the provider-specific implementation of the direct payment flow.
* *
* For an acquirer to redefine the processing of the direct payment flow, it must override * For a provider to redefine the processing of the direct payment flow, it must override
* this method. * this method.
* *
* @private * @private
* @param {string} provider - The provider of the acquirer * @param {string} code - The code of the provider
* @param {number} acquirerId - The id of the acquirer handling the transaction * @param {number} providerId - The id of the provider handling the transaction
* @param {object} processingValues - The processing values of the transaction * @param {object} processingValues - The processing values of the transaction
* @return {Promise} * @return {Promise}
*/ */
_processDirectPayment: (provider, acquirerId, processingValues) => Promise.resolve(), _processDirectPayment: (code, providerId, processingValues) => Promise.resolve(),
/** /**
* Redirect the customer by submitting the redirect form included in the processing values. * Redirect the customer by submitting the redirect form included in the processing values.
* *
* For an acquirer to redefine the processing of the payment with redirection flow, it must * For a provider to redefine the processing of the payment with redirection flow, it must
* override this method. * override this method.
* *
* @private * @private
* @param {string} provider - The provider of the acquirer * @param {string} code - The code of the provider
* @param {number} acquirerId - The id of the acquirer handling the transaction * @param {number} providerId - The id of the provider handling the transaction
* @param {object} processingValues - The processing values of the transaction * @param {object} processingValues - The processing values of the transaction
* @return {undefined} * @return {undefined}
*/ */
_processRedirectPayment: (provider, acquirerId, processingValues) => { _processRedirectPayment: (code, providerId, processingValues) => {
// Append the redirect form to the body // Append the redirect form to the body
const $redirectForm = $(processingValues.redirect_form_html).attr( const $redirectForm = $(processingValues.redirect_form_html).attr(
'id', 'o_payment_redirect_form' 'id', 'o_payment_redirect_form'
@@ -404,16 +404,16 @@ odoo.define('payment.payment_form_mixin', require => {
/** /**
* Redirect the customer to the status route. * Redirect the customer to the status route.
* *
* For an acquirer to redefine the processing of the payment by token flow, it must override * For a provider to redefine the processing of the payment by token flow, it must override
* this method. * this method.
* *
* @private * @private
* @param {string} provider - The provider of the token's acquirer * @param {string} provider_code - The code of the token's provider
* @param {number} tokenId - The id of the token handling the transaction * @param {number} tokenId - The id of the token handling the transaction
* @param {object} processingValues - The processing values of the transaction * @param {object} processingValues - The processing values of the transaction
* @return {undefined} * @return {undefined}
*/ */
_processTokenPayment: (provider, tokenId, processingValues) => { _processTokenPayment: (provider_code, tokenId, processingValues) => {
// The flow is already completed as payments by tokens are immediately processed // The flow is already completed as payments by tokens are immediately processed
window.location = '/payment/status'; window.location = '/payment/status';
}, },
@@ -421,7 +421,7 @@ odoo.define('payment.payment_form_mixin', require => {
/** /**
* Set the online payment flow for the selected payment option. * Set the online payment flow for the selected payment option.
* *
* For an acquirer to manage direct payments, it must call this method from within its * For a provider to manage direct payments, it must call this method from within its
* override of `_prepareInlineForm` to declare its payment flow for the selected payment * override of `_prepareInlineForm` to declare its payment flow for the selected payment
* option. * option.
* *
@@ -460,7 +460,7 @@ odoo.define('payment.payment_form_mixin', require => {
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
/** /**
* Hide all extra payment icons of the acquirer linked to the clicked button. * Hide all extra payment icons of the provider linked to the clicked button.
* *
* Called when clicking on the "show less" button. * Called when clicking on the "show less" button.
* *
@@ -480,7 +480,7 @@ odoo.define('payment.payment_form_mixin', require => {
}, },
/** /**
* Display all the payment icons of the acquirer linked to the clicked button. * Display all the payment icons of the provider linked to the clicked button.
* *
* Called when clicking on the "show more" button. * Called when clicking on the "show more" button.
* *
@@ -1,5 +1,5 @@
.o_form_view { .o_form_view {
.o_payment_acquirer_desc { .o_payment_provider_desc {
margin-top: 10px; margin-top: 10px;
ul { ul {
list-style-type: none; list-style-type: none;
@@ -53,7 +53,7 @@
<small class="list-group-item-text"> <small class="list-group-item-text">
<t t-if="tx['display_message']"> <t t-if="tx['display_message']">
<!-- display_message is the content of the HTML field associated <!-- display_message is the content of the HTML field associated
with the current transaction state, set on the acquirer. --> with the current transaction state, set on the provider. -->
<t t-out="tx['display_message']"/> <t t-out="tx['display_message']"/>
</t> </t>
<t t-else=""> <t t-else="">
@@ -74,7 +74,7 @@
<small class="list-group-item-text"> <small class="list-group-item-text">
<t t-if="tx['display_message']"> <t t-if="tx['display_message']">
<!-- display_message is the content of the HTML field associated <!-- display_message is the content of the HTML field associated
with the current transaction state, set on the acquirer. --> with the current transaction state, set on the provider. -->
<t t-out="tx['display_message']"/> <t t-out="tx['display_message']"/>
</t> </t>
<t t-else=""> <t t-else="">
@@ -99,11 +99,11 @@
<small class="list-group-item-text"> <small class="list-group-item-text">
<t t-if="tx['display_message']"> <t t-if="tx['display_message']">
<!-- display_message is the content of the HTML field associated <!-- display_message is the content of the HTML field associated
with the current transaction state, set on the acquirer. --> with the current transaction state, set on the provider. -->
<t t-out="tx['display_message']"/> <t t-out="tx['display_message']"/>
</t> </t>
<t t-else=""> <t t-else="">
We are waiting for the payment acquirer to confirm the payment. We are waiting for the payment provider to confirm the payment.
</t> </t>
</small> </small>
</a> </a>
+1 -1
View File
@@ -4,6 +4,6 @@ from . import common
from . import http_common from . import http_common
from . import test_flows from . import test_flows
from . import test_multicompany_flows from . import test_multicompany_flows
from . import test_payment_acquirer from . import test_payment_provider
from . import test_payment_token from . import test_payment_token
from . import test_payment_transaction from . import test_payment_transaction
+29 -29
View File
@@ -59,7 +59,7 @@ class PaymentCommon(TransactionCase):
'country_id': cls.country_belgium.id, 'country_id': cls.country_belgium.id,
}) })
# Create a dummy acquirer to allow basic tests without any specific acquirer implementation # Create a dummy provider to allow basic tests without any specific provider implementation
arch = """ arch = """
<form action="dummy" method="post"> <form action="dummy" method="post">
<input type="hidden" name="view_id" t-att-value="viewid"/> <input type="hidden" name="view_id" t-att-value="viewid"/>
@@ -72,16 +72,16 @@ class PaymentCommon(TransactionCase):
'arch': arch, 'arch': arch,
}) })
cls.dummy_acquirer = cls.env['payment.acquirer'].create({ cls.dummy_provider = cls.env['payment.provider'].create({
'name': "Dummy Acquirer", 'name': "Dummy Provider",
'provider': 'none', 'code': 'none',
'state': 'test', 'state': 'test',
'is_published': True, 'is_published': True,
'allow_tokenization': True, 'allow_tokenization': True,
'redirect_form_view_id': redirect_form.id, 'redirect_form_view_id': redirect_form.id,
}) })
cls.acquirer = cls.dummy_acquirer cls.provider = cls.dummy_provider
cls.amount = 1111.11 cls.amount = 1111.11
cls.company = cls.env.company cls.company = cls.env.company
cls.company_id = cls.company.id cls.company_id = cls.company.id
@@ -102,7 +102,7 @@ class PaymentCommon(TransactionCase):
super().setUp() super().setUp()
if self.account_payment_installed: if self.account_payment_installed:
# disable account payment generation if account_payment is installed # disable account payment generation if account_payment is installed
# because the accounting setup of acquirers is not managed in this common # because the accounting setup of providers is not managed in this common
self.reconcile_after_done_patcher = patch( self.reconcile_after_done_patcher = patch(
'odoo.addons.account_payment.models.payment_transaction.PaymentTransaction._reconcile_after_done', 'odoo.addons.account_payment.models.payment_transaction.PaymentTransaction._reconcile_after_done',
) )
@@ -120,44 +120,44 @@ class PaymentCommon(TransactionCase):
return currency return currency
@classmethod @classmethod
def _prepare_acquirer(cls, provider='none', company=None, update_values=None): def _prepare_provider(cls, code='none', company=None, update_values=None):
""" Prepare and return the first acquirer matching the given provider and company. """ Prepare and return the first provider matching the given provider and company.
If no acquirer is found in the given company, we duplicate the one from the base company. If no provider is found in the given company, we duplicate the one from the base company.
All other acquirers belonging to the same company are disabled to avoid any interferences. All other providers belonging to the same company are disabled to avoid any interferences.
:param str provider: The provider of the acquirer to prepare :param str code: The code of the provider to prepare
:param recordset company: The company of the acquirer to prepare, as a `res.company` record :param recordset company: The company of the provider to prepare, as a `res.company` record
:param dict update_values: The values used to update the acquirer :param dict update_values: The values used to update the provider
:return: The acquirer to prepare, if found :return: The provider to prepare, if found
:rtype: recordset of `payment.acquirer` :rtype: recordset of `payment.provider`
""" """
company = company or cls.env.company company = company or cls.env.company
update_values = update_values or {} update_values = update_values or {}
acquirer = cls.env['payment.acquirer'].sudo().search( provider = cls.env['payment.provider'].sudo().search(
[('provider', '=', provider), ('company_id', '=', company.id)], limit=1 [('code', '=', code), ('company_id', '=', company.id)], limit=1
) )
if not acquirer: if not provider:
base_acquirer = cls.env['payment.acquirer'].sudo().search( base_provider = cls.env['payment.provider'].sudo().search(
[('provider', '=', provider)], limit=1 [('code', '=', code)], limit=1
) )
if not base_acquirer: if not base_provider:
_logger.error("no payment.acquirer found for provider %s", provider) _logger.error("no payment.provider found for code %s", code)
return cls.env['payment.acquirer'] return cls.env['payment.provider']
else: else:
acquirer = base_acquirer.copy({'company_id': company.id}) provider = base_provider.copy({'company_id': company.id})
update_values['state'] = 'test' update_values['state'] = 'test'
acquirer.write(update_values) provider.write(update_values)
return acquirer return provider
def _create_transaction(self, flow, sudo=True, **values): def _create_transaction(self, flow, sudo=True, **values):
default_values = { default_values = {
'amount': self.amount, 'amount': self.amount,
'currency_id': self.currency.id, 'currency_id': self.currency.id,
'acquirer_id': self.acquirer.id, 'provider_id': self.provider.id,
'reference': self.reference, 'reference': self.reference,
'operation': f'online_{flow}', 'operation': f'online_{flow}',
'partner_id': self.partner.id, 'partner_id': self.partner.id,
@@ -167,9 +167,9 @@ class PaymentCommon(TransactionCase):
def _create_token(self, sudo=True, **values): def _create_token(self, sudo=True, **values):
default_values = { default_values = {
'payment_details': "1234", 'payment_details': "1234",
'acquirer_id': self.acquirer.id, 'provider_id': self.provider.id,
'partner_id': self.partner.id, 'partner_id': self.partner.id,
'acquirer_ref': "Acquirer Ref (TEST)", 'provider_ref': "provider Ref (TEST)",
'active': True, 'active': True,
} }
return self.env['payment.token'].sudo(sudo).create(dict(default_values, **values)) return self.env['payment.token'].sudo(sudo).create(dict(default_values, **values))
+7 -7
View File
@@ -85,12 +85,12 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
}) })
def _get_tx_context(self, response, form_name): def _get_tx_context(self, response, form_name):
"""Extracts txContext & other form info (acquirer & token ids) """Extracts txContext & other form info (provider & token ids)
from a payment response (with manage/checkout html form) from a payment response (with manage/checkout html form)
:param response: http Response, with a payment form as text :param response: http Response, with a payment form as text
:param str form_name: o_payment_manage / o_payment_checkout :param str form_name: o_payment_manage / o_payment_checkout
:return: Transaction context (+ acquirer_ids & token_ids) :return: Transaction context (+ provider_ids & token_ids)
:rtype: dict :rtype: dict
""" """
# Need to specify an HTML parser as parser # Need to specify an HTML parser as parser
@@ -114,20 +114,20 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
values[formatted_key] = formatted_val values[formatted_key] = formatted_val
payment_options_inputs = html_tree.xpath("//input[@name='o_payment_radio']") payment_options_inputs = html_tree.xpath("//input[@name='o_payment_radio']")
acquirer_ids = [] provider_ids = []
token_ids = [] token_ids = []
for p_o_input in payment_options_inputs: for p_o_input in payment_options_inputs:
data = dict() data = dict()
for key, val in p_o_input.items(): for key, val in p_o_input.items():
if key.startswith('data-'): if key.startswith('data-'):
data[key[5:]] = val data[key[5:]] = val
if data['payment-option-type'] == 'acquirer': if data['payment-option-type'] == 'provider':
acquirer_ids.append(int(data['payment-option-id'])) provider_ids.append(int(data['payment-option-id']))
else: else:
token_ids.append(int(data['payment-option-id'])) token_ids.append(int(data['payment-option-id']))
values.update({ values.update({
'acquirer_ids': acquirer_ids, 'provider_ids': provider_ids,
'token_ids': token_ids, 'token_ids': token_ids,
}) })
@@ -199,7 +199,7 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
""" Prepare the basic payment/transaction route values. """ Prepare the basic payment/transaction route values.
:param int payment_option_id: The payment option handling the transaction, as a :param int payment_option_id: The payment option handling the transaction, as a
`payment.acquirer` id or a `payment.token` id `payment.provider` id or a `payment.token` id
:param str flow: The payment flow :param str flow: The payment flow
:return: The route values :return: The route values
:rtype: dict :rtype: dict
+30 -30
View File
@@ -31,7 +31,7 @@ class TestFlows(PaymentHttpCommon):
if key in route_values: if key in route_values:
self.assertEqual(val, route_values[key]) self.assertEqual(val, route_values[key])
self.assertIn(self.acquirer.id, tx_context['acquirer_ids']) self.assertIn(self.provider.id, tx_context['provider_ids'])
# Route values are taken from tx_context result of /pay route to correctly simulate the flow # Route values are taken from tx_context result of /pay route to correctly simulate the flow
route_values = { route_values = {
@@ -47,7 +47,7 @@ class TestFlows(PaymentHttpCommon):
} }
route_values.update({ route_values.update({
'flow': flow, 'flow': flow,
'payment_option_id': self.acquirer.id, 'payment_option_id': self.provider.id,
'tokenization_requested': False, 'tokenization_requested': False,
}) })
@@ -59,14 +59,14 @@ class TestFlows(PaymentHttpCommon):
tx_sudo = self._get_tx(processing_values['reference']) tx_sudo = self._get_tx(processing_values['reference'])
# Tx values == given values # Tx values == given values
self.assertEqual(tx_sudo.acquirer_id.id, self.acquirer.id) self.assertEqual(tx_sudo.provider_id.id, self.provider.id)
self.assertEqual(tx_sudo.amount, self.amount) self.assertEqual(tx_sudo.amount, self.amount)
self.assertEqual(tx_sudo.currency_id.id, self.currency.id) self.assertEqual(tx_sudo.currency_id.id, self.currency.id)
self.assertEqual(tx_sudo.partner_id.id, self.partner.id) self.assertEqual(tx_sudo.partner_id.id, self.partner.id)
self.assertEqual(tx_sudo.reference, self.reference) self.assertEqual(tx_sudo.reference, self.reference)
# processing_values == given values # processing_values == given values
self.assertEqual(processing_values['acquirer_id'], self.acquirer.id) self.assertEqual(processing_values['provider_id'], self.provider.id)
self.assertEqual(processing_values['amount'], self.amount) self.assertEqual(processing_values['amount'], self.amount)
self.assertEqual(processing_values['currency_id'], self.currency.id) self.assertEqual(processing_values['currency_id'], self.currency.id)
self.assertEqual(processing_values['partner_id'], self.partner.id) self.assertEqual(processing_values['partner_id'], self.partner.id)
@@ -89,7 +89,7 @@ class TestFlows(PaymentHttpCommon):
str(self.user.id)) str(self.user.id))
self.assertEqual( self.assertEqual(
redirect_form_info['inputs']['view_id'], redirect_form_info['inputs']['view_id'],
str(self.dummy_acquirer.redirect_form_view_id.id)) str(self.dummy_provider.redirect_form_view_id.id))
return tx_sudo return tx_sudo
@@ -160,8 +160,8 @@ class TestFlows(PaymentHttpCommon):
# Fixed with freezegun # Fixed with freezegun
expected_reference = 'V-20111102120021' expected_reference = 'V-20111102120021'
validation_amount = self.acquirer._get_validation_amount() validation_amount = self.provider._get_validation_amount()
validation_currency = self.acquirer._get_validation_currency() validation_currency = self.provider._get_validation_currency()
tx_context = self._get_tx_manage_context() tx_context = self._get_tx_manage_context()
expected_values = { expected_values = {
@@ -179,7 +179,7 @@ class TestFlows(PaymentHttpCommon):
'partner_id': tx_context['partner_id'], 'partner_id': tx_context['partner_id'],
'access_token': tx_context['access_token'], 'access_token': tx_context['access_token'],
'flow': flow, 'flow': flow,
'payment_option_id': self.acquirer.id, 'payment_option_id': self.provider.id,
'tokenization_requested': True, 'tokenization_requested': True,
'reference_prefix': tx_context['reference_prefix'], 'reference_prefix': tx_context['reference_prefix'],
'landing_route': tx_context['landing_route'], 'landing_route': tx_context['landing_route'],
@@ -190,13 +190,13 @@ class TestFlows(PaymentHttpCommon):
tx_sudo = self._get_tx(processing_values['reference']) tx_sudo = self._get_tx(processing_values['reference'])
# Tx values == given values # Tx values == given values
self.assertEqual(tx_sudo.acquirer_id.id, self.acquirer.id) self.assertEqual(tx_sudo.provider_id.id, self.provider.id)
self.assertEqual(tx_sudo.amount, validation_amount) self.assertEqual(tx_sudo.amount, validation_amount)
self.assertEqual(tx_sudo.currency_id.id, validation_currency.id) self.assertEqual(tx_sudo.currency_id.id, validation_currency.id)
self.assertEqual(tx_sudo.partner_id.id, self.partner.id) self.assertEqual(tx_sudo.partner_id.id, self.partner.id)
self.assertEqual(tx_sudo.reference, expected_reference) self.assertEqual(tx_sudo.reference, expected_reference)
# processing_values == given values # processing_values == given values
self.assertEqual(processing_values['acquirer_id'], self.acquirer.id) self.assertEqual(processing_values['provider_id'], self.provider.id)
self.assertEqual(processing_values['amount'], validation_amount) self.assertEqual(processing_values['amount'], validation_amount)
self.assertEqual(processing_values['currency_id'], validation_currency.id) self.assertEqual(processing_values['currency_id'], validation_currency.id)
self.assertEqual(processing_values['partner_id'], self.partner.id) self.assertEqual(processing_values['partner_id'], self.partner.id)
@@ -281,7 +281,7 @@ class TestFlows(PaymentHttpCommon):
transaction_values = self._prepare_pay_values() transaction_values = self._prepare_pay_values()
transaction_values.update({ transaction_values.update({
'flow': 'this flow does not exist', 'flow': 'this flow does not exist',
'payment_option_id': self.acquirer.id, 'payment_option_id': self.provider.id,
'tokenization_requested': False, 'tokenization_requested': False,
'reference_prefix': 'whatever', 'reference_prefix': 'whatever',
'landing_route': 'whatever', 'landing_route': 'whatever',
@@ -304,37 +304,37 @@ class TestFlows(PaymentHttpCommon):
"odoo.exceptions.ValidationError: The access token is invalid.", "odoo.exceptions.ValidationError: The access token is invalid.",
response.text) response.text)
def test_access_disabled_acquirers_tokens(self): def test_access_disabled_providers_tokens(self):
self.partner = self.portal_partner self.partner = self.portal_partner
# Log in as user from Company A # Log in as user from Company A
self.authenticate(self.portal_user.login, self.portal_user.login) self.authenticate(self.portal_user.login, self.portal_user.login)
token = self._create_token() token = self._create_token()
acquirer_b = self.acquirer.copy() provider_b = self.provider.copy()
acquirer_b.state = 'test' provider_b.state = 'test'
token_b = self._create_token(acquirer_id=acquirer_b.id) token_b = self._create_token(provider_id=provider_b.id)
# User must see both enabled acquirers and tokens # User must see both enabled providers and tokens
manage_context = self._get_tx_manage_context() manage_context = self._get_tx_manage_context()
self.assertEqual(manage_context['partner_id'], self.partner.id) self.assertEqual(manage_context['partner_id'], self.partner.id)
self.assertIn(self.acquirer.id, manage_context['acquirer_ids']) self.assertIn(self.provider.id, manage_context['provider_ids'])
self.assertIn(acquirer_b.id, manage_context['acquirer_ids']) self.assertIn(provider_b.id, manage_context['provider_ids'])
self.assertIn(token.id, manage_context['token_ids']) self.assertIn(token.id, manage_context['token_ids'])
self.assertIn(token_b.id, manage_context['token_ids']) self.assertIn(token_b.id, manage_context['token_ids'])
# Token of disabled acquirer(s) & disabled acquirers should not be shown # Token of disabled provider(s) & disabled providers should not be shown
self.acquirer.state = 'disabled' self.provider.state = 'disabled'
manage_context = self._get_tx_manage_context() manage_context = self._get_tx_manage_context()
self.assertEqual(manage_context['partner_id'], self.partner.id) self.assertEqual(manage_context['partner_id'], self.partner.id)
self.assertEqual(manage_context['acquirer_ids'], [acquirer_b.id]) self.assertEqual(manage_context['provider_ids'], [provider_b.id])
self.assertEqual(manage_context['token_ids'], [token_b.id]) self.assertEqual(manage_context['token_ids'], [token_b.id])
# Archived tokens must be hidden from the user # Archived tokens must be hidden from the user
token_b.active = False token_b.active = False
manage_context = self._get_tx_manage_context() manage_context = self._get_tx_manage_context()
self.assertEqual(manage_context['partner_id'], self.partner.id) self.assertEqual(manage_context['partner_id'], self.partner.id)
self.assertEqual(manage_context['acquirer_ids'], [acquirer_b.id]) self.assertEqual(manage_context['provider_ids'], [provider_b.id])
self.assertEqual(manage_context['token_ids'], []) self.assertEqual(manage_context['token_ids'], [])
@mute_logger('odoo.addons.payment.models.payment_transaction') @mute_logger('odoo.addons.payment.models.payment_transaction')
@@ -347,7 +347,7 @@ class TestFlows(PaymentHttpCommon):
'._send_payment_request' '._send_payment_request'
) as patched: ) as patched:
self._portal_transaction( self._portal_transaction(
**self._prepare_transaction_values(self.acquirer.id, 'direct') **self._prepare_transaction_values(self.provider.id, 'direct')
) )
self.assertEqual(patched.call_count, 0) self.assertEqual(patched.call_count, 0)
@@ -361,7 +361,7 @@ class TestFlows(PaymentHttpCommon):
'._send_payment_request' '._send_payment_request'
) as patched: ) as patched:
self._portal_transaction( self._portal_transaction(
**self._prepare_transaction_values(self.acquirer.id, 'redirect') **self._prepare_transaction_values(self.provider.id, 'redirect')
) )
self.assertEqual(patched.call_count, 0) self.assertEqual(patched.call_count, 0)
@@ -380,15 +380,15 @@ class TestFlows(PaymentHttpCommon):
self.assertEqual(patched.call_count, 1) self.assertEqual(patched.call_count, 1)
def test_tokenization_input_is_show_to_logged_in_users(self): def test_tokenization_input_is_show_to_logged_in_users(self):
self.acquirer.allow_tokenization = True self.provider.allow_tokenization = True
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping( show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(
self.acquirer, logged_in=True self.provider, logged_in=True
) )
self.assertDictEqual(show_tokenize_input, {self.acquirer.id: True}) self.assertDictEqual(show_tokenize_input, {self.provider.id: True})
def test_tokenization_input_is_hidden_for_logged_out_users(self): def test_tokenization_input_is_hidden_for_logged_out_users(self):
self.acquirer.allow_tokenization = False self.provider.allow_tokenization = False
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping( show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(
self.acquirer, logged_in=True self.provider, logged_in=True
) )
self.assertDictEqual(show_tokenize_input, {self.acquirer.id: False}) self.assertDictEqual(show_tokenize_input, {self.provider.id: False})
@@ -35,7 +35,7 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
'groups_id': [Command.link(cls.group_user.id)], 'groups_id': [Command.link(cls.group_user.id)],
}) })
cls.acquirer_company_b = cls._prepare_acquirer(company=cls.company_b) cls.provider_company_b = cls._prepare_provider(company=cls.company_b)
def test_pay_logged_in_another_company(self): def test_pay_logged_in_another_company(self):
"""User pays for an amount in another company.""" """User pays for an amount in another company."""
@@ -59,9 +59,9 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
else: else:
self.assertEqual(val, route_values[key]) self.assertEqual(val, route_values[key])
available_acquirers = self.env['payment.acquirer'].sudo().browse(tx_context['acquirer_ids']) available_providers = self.env['payment.provider'].sudo().browse(tx_context['provider_ids'])
self.assertIn(self.acquirer_company_b, available_acquirers) self.assertIn(self.provider_company_b, available_providers)
self.assertEqual(available_acquirers.company_id, self.company_b) self.assertEqual(available_providers.company_id, self.company_b)
validation_values = { validation_values = {
k: tx_context[k] k: tx_context[k]
@@ -76,7 +76,7 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
} }
validation_values.update({ validation_values.update({
'flow': 'direct', 'flow': 'direct',
'payment_option_id': self.acquirer_company_b.id, 'payment_option_id': self.provider_company_b.id,
'tokenization_requested': False, 'tokenization_requested': False,
}) })
with mute_logger('odoo.addons.payment.models.payment_transaction'): with mute_logger('odoo.addons.payment.models.payment_transaction'):
@@ -84,14 +84,14 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
tx_sudo = self._get_tx(processing_values['reference']) tx_sudo = self._get_tx(processing_values['reference'])
# Tx values == given values # Tx values == given values
self.assertEqual(tx_sudo.acquirer_id.id, self.acquirer_company_b.id) self.assertEqual(tx_sudo.provider_id.id, self.provider_company_b.id)
self.assertEqual(tx_sudo.amount, self.amount) self.assertEqual(tx_sudo.amount, self.amount)
self.assertEqual(tx_sudo.currency_id.id, self.currency.id) self.assertEqual(tx_sudo.currency_id.id, self.currency.id)
self.assertEqual(tx_sudo.partner_id.id, self.user_company_a.partner_id.id) self.assertEqual(tx_sudo.partner_id.id, self.user_company_a.partner_id.id)
self.assertEqual(tx_sudo.reference, self.reference) self.assertEqual(tx_sudo.reference, self.reference)
self.assertEqual(tx_sudo.company_id, self.company_b) self.assertEqual(tx_sudo.company_id, self.company_b)
# processing_values == given values # processing_values == given values
self.assertEqual(processing_values['acquirer_id'], self.acquirer_company_b.id) self.assertEqual(processing_values['provider_id'], self.provider_company_b.id)
self.assertEqual(processing_values['amount'], self.amount) self.assertEqual(processing_values['amount'], self.amount)
self.assertEqual(processing_values['currency_id'], self.currency.id) self.assertEqual(processing_values['currency_id'], self.currency.id)
self.assertEqual(processing_values['partner_id'], self.user_company_a.partner_id.id) self.assertEqual(processing_values['partner_id'], self.user_company_a.partner_id.id)
@@ -104,13 +104,13 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
self.authenticate(self.portal_user.login, self.portal_user.login) self.authenticate(self.portal_user.login, self.portal_user.login)
token = self._create_token() token = self._create_token()
token_company_b = self._create_token(acquirer_id=self.acquirer_company_b.id) token_company_b = self._create_token(provider_id=self.provider_company_b.id)
# A partner should see all his tokens on the /my/payment_method route, # A partner should see all his tokens on the /my/payment_method route,
# even if they are in other companies otherwise he won't ever see them. # even if they are in other companies otherwise he won't ever see them.
manage_context = self._get_tx_manage_context() manage_context = self._get_tx_manage_context()
self.assertEqual(manage_context['partner_id'], self.partner.id) self.assertEqual(manage_context['partner_id'], self.partner.id)
self.assertEqual(manage_context['acquirer_ids'], self.acquirer.ids) self.assertEqual(manage_context['provider_ids'], self.provider.ids)
self.assertIn(token.id, manage_context['token_ids']) self.assertIn(token.id, manage_context['token_ids'])
self.assertIn(token_company_b.id, manage_context['token_ids']) self.assertIn(token_company_b.id, manage_context['token_ids'])
@@ -1,69 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentCommon
@tagged('-at_install', 'post_install')
class TestPaymentAcquirer(PaymentCommon):
def test_published_acquirer_compatible_with_all_users(self):
""" Test that a published acquirer is always available to all users. """
for user in (self.public_user, self.portal_user):
self.env = self.env(user=user)
compatible_acquirers = self.env['payment.acquirer'].sudo()._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount
)
self.assertIn(self.acquirer, compatible_acquirers)
def test_unpublished_acquirer_compatible_with_internal_user(self):
""" Test that an unpublished acquirer is still available to internal users. """
self.acquirer.is_published = False
compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount
)
self.assertIn(self.acquirer, compatible_acquirers)
def test_unpublished_acquirer_not_compatible_with_non_internal_user(self):
""" Test that an unpublished acquirer is not available to non-internal users. """
self.acquirer.is_published = False
for user in (self.public_user, self.portal_user):
self.env = self.env(user=user)
compatible_acquirers = self.env['payment.acquirer'].sudo()._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount
)
self.assertNotIn(self.acquirer, compatible_acquirers)
def test_acquirer_compatible_when_maximum_amount_is_zero(self):
""" Test that the maximum amount has no effect on the acquirer's compatibility when it is
set to 0. """
self.acquirer.maximum_amount = 0.
compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
)
self.assertIn(self.acquirer, compatible_acquirers)
def test_acquirer_compatible_when_payment_below_maximum_amount(self):
""" Test that an acquirer is compatible when the payment amount is less than the maximum
amount. """
self.acquirer.maximum_amount = self.amount + 10.0
compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
)
self.assertIn(self.acquirer, compatible_acquirers)
def test_acquirer_not_compatible_when_payment_above_maximum_amount(self):
""" Test that an acquirer is not compatible when the payment amount is more than the maximum
amount. """
self.acquirer.maximum_amount = self.amount - 10.0
compatible_acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
)
self.assertNotIn(self.acquirer, compatible_acquirers)
@@ -0,0 +1,69 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentCommon
@tagged('-at_install', 'post_install')
class TestPaymentprovider(PaymentCommon):
def test_published_provider_compatible_with_all_users(self):
""" Test that a published provider is always available to all users. """
for user in (self.public_user, self.portal_user):
self.env = self.env(user=user)
compatible_providers = self.env['payment.provider'].sudo()._get_compatible_providers(
self.company.id, self.partner.id, self.amount
)
self.assertIn(self.provider, compatible_providers)
def test_unpublished_provider_compatible_with_internal_user(self):
""" Test that an unpublished provider is still available to internal users. """
self.provider.is_published = False
compatible_providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount
)
self.assertIn(self.provider, compatible_providers)
def test_unpublished_provider_not_compatible_with_non_internal_user(self):
""" Test that an unpublished provider is not available to non-internal users. """
self.provider.is_published = False
for user in (self.public_user, self.portal_user):
self.env = self.env(user=user)
compatible_providers = self.env['payment.provider'].sudo()._get_compatible_providers(
self.company.id, self.partner.id, self.amount
)
self.assertNotIn(self.provider, compatible_providers)
def test_provider_compatible_when_maximum_amount_is_zero(self):
""" Test that the maximum amount has no effect on the provider's compatibility when it is
set to 0. """
self.provider.maximum_amount = 0.
compatible_providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
)
self.assertIn(self.provider, compatible_providers)
def test_provider_compatible_when_payment_below_maximum_amount(self):
""" Test that an provider is compatible when the payment amount is less than the maximum
amount. """
self.provider.maximum_amount = self.amount + 10.0
compatible_providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
)
self.assertIn(self.provider, compatible_providers)
def test_provider_not_compatible_when_payment_above_maximum_amount(self):
""" Test that an provider is not compatible when the payment amount is more than the maximum
amount. """
self.provider.maximum_amount = self.amount - 10.0
compatible_providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
)
self.assertNotIn(self.provider, compatible_providers)
@@ -9,7 +9,7 @@ from odoo.addons.payment.tests.common import PaymentCommon
class TestPaymentTransaction(PaymentCommon): class TestPaymentTransaction(PaymentCommon):
def test_refunds_count(self): def test_refunds_count(self):
self.acquirer.support_refund = 'full_only' # Should simply not be False self.provider.support_refund = 'full_only' # Should simply not be False
tx = self._create_transaction('redirect', state='done') tx = self._create_transaction('redirect', state='done')
for reference_index, operation in enumerate( for reference_index, operation in enumerate(
('online_redirect', 'online_direct', 'online_token', 'validation', 'refund') ('online_redirect', 'online_direct', 'online_token', 'validation', 'refund')
@@ -29,7 +29,7 @@ class TestPaymentTransaction(PaymentCommon):
) )
def test_refund_transaction_values(self): def test_refund_transaction_values(self):
self.acquirer.support_refund = 'partial' self.provider.support_refund = 'partial'
tx = self._create_transaction('redirect', state='done') tx = self._create_transaction('redirect', state='done')
# Test the default values of a full refund transaction # Test the default values of a full refund transaction
+2 -2
View File
@@ -12,8 +12,8 @@
<h1><field name="name" placeholder="Name"/></h1> <h1><field name="name" placeholder="Name"/></h1>
</div> </div>
<notebook> <notebook>
<page string="Acquirers list" name="acquirers"> <page string="Providers list" name="providers">
<field nolabel="1" name="acquirer_ids"/> <field nolabel="1" name="provider_ids"/>
</page> </page>
</notebook> </notebook>
</sheet> </sheet>
@@ -5,11 +5,11 @@
<template id="pay_meth_link" inherit_id="portal.portal_layout"> <template id="pay_meth_link" inherit_id="portal.portal_layout">
<xpath expr="//div[hasclass('o_portal_my_details')]" position="inside"> <xpath expr="//div[hasclass('o_portal_my_details')]" position="inside">
<t t-set="partner" t-value="request.env.user.partner_id"/> <t t-set="partner" t-value="request.env.user.partner_id"/>
<t t-set="acquirers_allowing_tokenization" <t t-set="providers_allowing_tokenization"
t-value="request.env['payment.acquirer'].sudo()._get_compatible_acquirers(request.env.company.id, partner.id, 0., force_tokenization=True, is_validation=True)"/> t-value="request.env['payment.provider'].sudo()._get_compatible_providers(request.env.company.id, partner.id, 0., force_tokenization=True, is_validation=True)"/>
<t t-set="existing_tokens" t-value="partner.payment_token_ids + partner.commercial_partner_id.sudo().payment_token_ids"/> <t t-set="existing_tokens" t-value="partner.payment_token_ids + partner.commercial_partner_id.sudo().payment_token_ids"/>
<!-- Only show the link if a token can be created or if one already exists --> <!-- Only show the link if a token can be created or if one already exists -->
<div t-if="acquirers_allowing_tokenization or existing_tokens" <div t-if="providers_allowing_tokenization or existing_tokens"
class='manage_payment_method mt16'> class='manage_payment_method mt16'>
<a href="/my/payment_method">Manage payment methods</a> <a href="/my/payment_method">Manage payment methods</a>
</div> </div>
@@ -40,7 +40,7 @@
<div t-elif="not partner_id" class="alert alert-warning"> <div t-elif="not partner_id" class="alert alert-warning">
<strong>Warning</strong> You must be logged in to pay. <strong>Warning</strong> You must be logged in to pay.
</div> </div>
<div t-elif="not acquirers and not tokens" class="alert alert-warning"> <div t-elif="not providers and not tokens" class="alert alert-warning">
<strong>No suitable payment option could be found.</strong><br/> <strong>No suitable payment option could be found.</strong><br/>
If you believe that it is an error, please contact the website administrator. If you believe that it is an error, please contact the website administrator.
</div> </div>
@@ -76,9 +76,9 @@
<!-- Manage page --> <!-- Manage page -->
<div class="row"> <div class="row">
<div class="col-lg-7"> <div class="col-lg-7">
<t t-if="acquirers or tokens" t-call="payment.manage"/> <t t-if="providers or tokens" t-call="payment.manage"/>
<div t-else="" class="alert alert-warning"> <div t-else="" class="alert alert-warning">
<p><strong>No suitable payment acquirer could be found.</strong></p> <p><strong>No suitable payment provider could be found.</strong></p>
<p>If you believe that it is an error, please contact the website administrator.</p> <p>If you believe that it is an error, please contact the website administrator.</p>
</div> </div>
</div> </div>
@@ -156,7 +156,7 @@
<hr/> <hr/>
<div class="row"> <div class="row">
<div class="col-md-5 text-muted"> <div class="col-md-5 text-muted">
Processed by <t t-esc="tx.acquirer_id.sudo().name"/> Processed by <t t-esc="tx.provider_id.sudo().name"/>
</div> </div>
<div class="col-md-4 offset-md-3 mt-2 ps-0"> <div class="col-md-4 offset-md-3 mt-2 ps-0">
<a role="button" <a role="button"
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<record id="payment_acquirer_form" model="ir.ui.view"> <record id="payment_provider_form" model="ir.ui.view">
<field name="name">payment.acquirer.form</field> <field name="name">payment.provider.form</field>
<field name="model">payment.acquirer</field> <field name="model">payment.provider</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Payment Acquirer"> <form string="Payment provider">
<field name="company_id" invisible="1"/> <field name="company_id" invisible="1"/>
<field name="is_published" invisible="1"/> <field name="is_published" invisible="1"/>
<field name="main_currency_id" invisible="1"/> <field name="main_currency_id" invisible="1"/>
@@ -25,7 +25,7 @@
<field name="show_auth_msg" invisible="1"/> <field name="show_auth_msg" invisible="1"/>
<field name="show_done_msg" invisible="1"/> <field name="show_done_msg" invisible="1"/>
<field name="show_cancel_msg" invisible="1"/> <field name="show_cancel_msg" invisible="1"/>
<field name="provider" invisible="1"/> <field name="code" invisible="1"/>
<sheet> <sheet>
<!-- === Stat Buttons === --> <!-- === Stat Buttons === -->
<div class="oe_button_box" name="button_box"> <div class="oe_button_box" name="button_box">
@@ -59,24 +59,24 @@
</div> </div>
</div> </div>
<div id="provider_creation_warning" attrs="{'invisible': [('id', '!=', False)]}" class="alert alert-warning" role="alert"> <div id="provider_creation_warning" attrs="{'invisible': [('id', '!=', False)]}" class="alert alert-warning" role="alert">
<strong>Warning</strong> Creating a payment acquirer from the <em>CREATE</em> button is not supported. <strong>Warning</strong> Creating a payment provider from the <em>CREATE</em> button is not supported.
Please use the <em>Duplicate</em> action instead. Please use the <em>Duplicate</em> action instead.
</div> </div>
<group> <group>
<group name="payment_state"> <group name="payment_state">
<field name="provider" groups="base.group_no_one" attrs="{'readonly': [('id', '!=', False)], 'invisible': [('module_id', '!=', False), ('module_state', '!=', 'installed')]}"/> <field name="code" groups="base.group_no_one" attrs="{'readonly': [('id', '!=', False)], 'invisible': [('module_id', '!=', False), ('module_state', '!=', 'installed')]}"/>
<field name="state" widget="radio" attrs="{'invisible': [('module_state', '=', 'uninstalled')]}"/> <field name="state" widget="radio" attrs="{'invisible': [('module_state', '=', 'uninstalled')]}"/>
<field name="company_id" groups="base.group_multi_company" options='{"no_open":True}'/> <field name="company_id" groups="base.group_multi_company" options='{"no_open":True}'/>
</group> </group>
</group> </group>
<notebook attrs="{'invisible': ['&amp;', ('module_id', '!=', False), ('module_state', '!=', 'installed')]}"> <notebook attrs="{'invisible': ['&amp;', ('module_id', '!=', False), ('module_state', '!=', 'installed')]}">
<page string="Credentials" name="acquirer_credentials" attrs="{'invisible': ['|', ('provider', '=', 'none'), ('show_credentials_page', '=', False)]}"> <page string="Credentials" name="provider_credentials" attrs="{'invisible': ['|', ('code', '=', 'none'), ('show_credentials_page', '=', False)]}">
<group name="acquirer"/> <group name="code"/>
</page> </page>
<page string="Configuration" name="configuration"> <page string="Configuration" name="configuration">
<group name="acquirer_config"> <group name="provider_config">
<group string="Payment Form" name="payment_form"> <group string="Payment Form" name="payment_form">
<field name="display_as" placeholder="If not defined, the acquirer name will be used."/> <field name="display_as" placeholder="If not defined, the provider name will be used."/>
<field name="payment_icon_ids" attrs="{'invisible': [('show_payment_icon_ids', '=', False)]}" widget="many2many_tags"/> <field name="payment_icon_ids" attrs="{'invisible': [('show_payment_icon_ids', '=', False)]}" widget="many2many_tags"/>
<field name="allow_tokenization" attrs="{'invisible': ['|', ('support_tokenization', '=', False), ('show_allow_tokenization', '=', False)]}"/> <field name="allow_tokenization" attrs="{'invisible': ['|', ('support_tokenization', '=', False), ('show_allow_tokenization', '=', False)]}"/>
<field name="capture_manually" attrs="{'invisible': [('support_manual_capture', '=', False)]}"/> <field name="capture_manually" attrs="{'invisible': [('support_manual_capture', '=', False)]}"/>
@@ -118,14 +118,14 @@
</field> </field>
</record> </record>
<record id="payment_acquirer_list" model="ir.ui.view"> <record id="payment_provider_list" model="ir.ui.view">
<field name="name">payment.acquirer.list</field> <field name="name">payment.provider.list</field>
<field name="model">payment.acquirer</field> <field name="model">payment.provider</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Payment Acquirers" create="false"> <tree string="Payment Providers" create="false">
<field name="sequence" widget="handle"/> <field name="sequence" widget="handle"/>
<field name="name"/> <field name="name"/>
<field name="provider"/> <field name="code"/>
<field name="state"/> <field name="state"/>
<field name="available_country_ids" widget="many2many_tags" optional="hide"/> <field name="available_country_ids" widget="many2many_tags" optional="hide"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/> <field name="company_id" groups="base.group_multi_company" optional="show"/>
@@ -133,16 +133,16 @@
</field> </field>
</record> </record>
<record id="payment_acquirer_kanban" model="ir.ui.view"> <record id="payment_provider_kanban" model="ir.ui.view">
<field name="name">payment.acquirer.kanban</field> <field name="name">payment.provider.kanban</field>
<field name="model">payment.acquirer</field> <field name="model">payment.provider</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<kanban create="false" quick_create="false" class="o_kanban_dashboard"> <kanban create="false" quick_create="false" class="o_kanban_dashboard">
<field name="id"/> <field name="id"/>
<field name="name"/> <field name="name"/>
<field name="state"/> <field name="state"/>
<field name="is_published"/> <field name="is_published"/>
<field name="provider"/> <field name="code"/>
<field name="module_id"/> <field name="module_id"/>
<field name="module_state"/> <field name="module_state"/>
<field name="module_to_buy"/> <field name="module_to_buy"/>
@@ -155,11 +155,11 @@
<t t-set="is_published" t-value="record.is_published.raw_value === true"/> <t t-set="is_published" t-value="record.is_published.raw_value === true"/>
<t t-set="to_upgrade" t-value="!installed and to_buy"/> <t t-set="to_upgrade" t-value="!installed and to_buy"/>
<div t-attf-class="oe_kanban_global_click" class="d-flex p-2"> <div t-attf-class="oe_kanban_global_click" class="d-flex p-2">
<div class="o_payment_acquirer_desc d-flex gap-2"> <div class="o_payment_provider_desc d-flex gap-2">
<img type="open" <img type="open"
t-att-src="kanban_image('payment.acquirer', 'image_128', record.id.raw_value)" t-att-src="kanban_image('payment.provider', 'image_128', record.id.raw_value)"
class="mb-0 o_image_64_max" class="mb-0 o_image_64_max"
alt="Acquirer"/> alt="provider"/>
<div class="d-flex flex-column justify-content-between w-100"> <div class="d-flex flex-column justify-content-between w-100">
<div class="o_payment_kanban_info"> <div class="o_payment_kanban_info">
<h4 class="mb-0"><t t-esc="record.name.value"/></h4> <h4 class="mb-0"><t t-esc="record.name.value"/></h4>
@@ -194,16 +194,16 @@
</field> </field>
</record> </record>
<record id="payment_acquirer_search" model="ir.ui.view"> <record id="payment_provider_search" model="ir.ui.view">
<field name="name">payment.acquirer.search</field> <field name="name">payment.provider.search</field>
<field name="model">payment.acquirer</field> <field name="model">payment.provider</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search> <search>
<field name="name" string="Acquirer" filter_domain="[('name', 'ilike', self)]"/> <field name="name" string="provider" filter_domain="[('name', 'ilike', self)]"/>
<field name="provider"/> <field name="code"/>
<filter name="acquirer_installed" string="Installed" domain="[('module_state', '=', 'installed')]"/> <filter name="provider_installed" string="Installed" domain="[('module_state', '=', 'installed')]"/>
<group expand="0" string="Group By"> <group expand="0" string="Group By">
<filter string="Provider" name="provider" context="{'group_by': 'provider'}"/> <filter string="Provider" name="code" context="{'group_by': 'code'}"/>
<filter string="State" name="state" context="{'group_by': 'state'}"/> <filter string="State" name="state" context="{'group_by': 'state'}"/>
<filter string="Company" name="company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/> <filter string="Company" name="company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
</group> </group>
@@ -211,13 +211,13 @@
</field> </field>
</record> </record>
<record id="action_payment_acquirer" model="ir.actions.act_window"> <record id="action_payment_provider" model="ir.actions.act_window">
<field name="name">Payment Acquirers</field> <field name="name">Payment Providers</field>
<field name="res_model">payment.acquirer</field> <field name="res_model">payment.provider</field>
<field name="view_mode">kanban,tree,form</field> <field name="view_mode">kanban,tree,form</field>
<field name="help" type="html"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Create a new payment acquirer Create a new payment provider
</p> </p>
</field> </field>
</record> </record>
+69 -69
View File
@@ -4,10 +4,10 @@
<!-- Checkout form --> <!-- Checkout form -->
<template id="checkout" name="Payment Checkout"> <template id="checkout" name="Payment Checkout">
<!-- Variables description: <!-- Variables description:
- 'acquirers' - The payment acquirers compatible with the current transaction - 'providers' - The payment providers compatible with the current transaction
- 'tokens' - The payment tokens of the current partner and payment acquirers - 'tokens' - The payment tokens of the current partner and payment providers
- 'default_token_id' - The id of the token that should be pre-selected. Optional - 'default_token_id' - The id of the token that should be pre-selected. Optional
- 'fees_by_acquirer' - The dict of transaction fees for each acquirer. Optional - 'fees_by_provider' - The dict of transaction fees for each provider. Optional
- 'show_tokenize_input' - Whether the option to save the payment method is shown - 'show_tokenize_input' - Whether the option to save the payment method is shown
- 'reference_prefix' - The custom prefix to compute the full transaction reference - 'reference_prefix' - The custom prefix to compute the full transaction reference
- 'amount' - The amount to pay. Optional (sale_subscription) - 'amount' - The amount to pay. Optional (sale_subscription)
@@ -29,50 +29,50 @@
t-att-data-landing-route="landing_route" t-att-data-landing-route="landing_route"
t-att-data-allow-token-selection="True"> t-att-data-allow-token-selection="True">
<t t-set="acquirer_count" t-value="len(acquirers) if acquirers else 0"/> <t t-set="provider_count" t-value="len(providers) if providers else 0"/>
<t t-set="token_count" t-value="len(tokens) if tokens else 0"/> <t t-set="token_count" t-value="len(tokens) if tokens else 0"/>
<!-- Check the radio button of the default token, if set, or of the first acquirer if <!-- Check the radio button of the default token, if set, or of the first provider if
it is the only payment option --> it is the only payment option -->
<t t-set="default_payment_option_id" <t t-set="default_payment_option_id"
t-value="default_token_id if default_token_id and token_count > 0 t-value="default_token_id if default_token_id and token_count > 0
else acquirers[0].id if acquirer_count == 1 and token_count == 0 else providers[0].id if provider_count == 1 and token_count == 0
else None"/> else None"/>
<t t-set="fees_by_acquirer" t-value="fees_by_acquirer or dict()"/> <t t-set="fees_by_provider" t-value="fees_by_provider or dict()"/>
<t t-set="footer_template_id" <t t-set="footer_template_id"
t-value="footer_template_id or 'payment.footer'"/> t-value="footer_template_id or 'payment.footer'"/>
<div class="card"> <div class="card">
<!-- === Acquirers === --> <!-- === Providers === -->
<t t-foreach="acquirers" t-as="acquirer"> <t t-foreach="providers" t-as="provider">
<div name="o_payment_option_card" class="card-body o_payment_option_card"> <div name="o_payment_option_card" class="card-body o_payment_option_card">
<label> <label>
<!-- === Radio button === --> <!-- === Radio button === -->
<!-- Only shown if linked to the only payment option --> <!-- Only shown if linked to the only payment option -->
<input name="o_payment_radio" <input name="o_payment_radio"
type="radio" type="radio"
t-att-checked="acquirer.id == default_payment_option_id" t-att-checked="provider.id == default_payment_option_id"
t-att-class="'' if acquirer_count + token_count > 1 else 'd-none'" t-att-class="'' if provider_count + token_count > 1 else 'd-none'"
t-att-data-payment-option-id="acquirer.id" t-att-data-payment-option-id="provider.id"
t-att-data-provider="acquirer.provider" t-att-data-provider="provider.code"
data-payment-option-type="acquirer"/> data-payment-option-type="provider"/>
<!-- === Acquirer name === --> <!-- === Provider name === -->
<span class="payment_option_name"> <span class="payment_option_name">
<b t-esc="acquirer.display_as or acquirer.name"/> <b t-esc="provider.display_as or provider.name"/>
</span> </span>
<!-- === "Test Mode" badge === --> <!-- === "Test Mode" badge === -->
<span t-if="acquirer.state == 'test'" <span t-if="provider.state == 'test'"
class="rounded-pill text-bg-warning ms-1"> class="rounded-pill text-bg-warning ms-1">
Test Mode Test Mode
</span> </span>
<!-- === "Unpublished" badge === --> <!-- === "Unpublished" badge === -->
<span t-if="not acquirer.is_published" <span t-if="not provider.is_published"
class="badge text-bg-danger ms-1"> class="badge text-bg-danger ms-1">
Unpublished Unpublished
</span> </span>
<!-- === Extra fees badge === --> <!-- === Extra fees badge === -->
<t t-if="fees_by_acquirer.get(acquirer)"> <t t-if="fees_by_provider.get(provider)">
<span class="rounded-pill text-bg-secondary ms-1"> <span class="rounded-pill text-bg-secondary ms-1">
+ <t t-esc="fees_by_acquirer.get(acquirer)" + <t t-esc="fees_by_provider.get(provider)"
t-options="{'widget': 'monetary', 'display_currency': currency}"/> t-options="{'widget': 'monetary', 'display_currency': currency}"/>
Fees Fees
</span> </span>
@@ -81,26 +81,26 @@
<!-- === Payment icon list === --> <!-- === Payment icon list === -->
<t t-call="payment.icon_list"/> <t t-call="payment.icon_list"/>
<!-- === Help message === --> <!-- === Help message === -->
<div t-if="not is_html_empty(acquirer.pre_msg)" <div t-if="not is_html_empty(provider.pre_msg)"
t-out="acquirer.pre_msg" t-out="provider.pre_msg"
class="text-muted ms-3"/> class="text-muted ms-3"/>
</div> </div>
<!-- === Acquirer inline form === --> <!-- === Provider inline form === -->
<div t-attf-id="o_payment_acquirer_inline_form_{{acquirer.id}}" <div t-attf-id="o_payment_provider_inline_form_{{provider.id}}"
name="o_payment_inline_form" name="o_payment_inline_form"
class="card-footer px-3 d-none"> class="card-footer px-3 d-none">
<t t-if="acquirer.sudo()._should_build_inline_form(is_validation=False)"> <t t-if="provider.sudo()._should_build_inline_form(is_validation=False)">
<t t-set="inline_form_xml_id" <t t-set="inline_form_xml_id"
t-value="acquirer.sudo().inline_form_view_id.xml_id"/> t-value="provider.sudo().inline_form_view_id.xml_id"/>
<!-- === Inline form content (filled by acquirer) === --> <!-- === Inline form content (filled by provider) === -->
<div t-if="inline_form_xml_id" class="clearfix"> <div t-if="inline_form_xml_id" class="clearfix">
<t t-call="{{inline_form_xml_id}}"> <t t-call="{{inline_form_xml_id}}">
<t t-set="acquirer_id" t-value="acquirer.id"/> <t t-set="provider_id" t-value="provider.id"/>
</t> </t>
</div> </div>
</t> </t>
<!-- === "Save my payment details" checkbox === --> <!-- === "Save my payment details" checkbox === -->
<label t-if="show_tokenize_input[acquirer.id]"> <label t-if="show_tokenize_input[provider.id]">
<input name="o_payment_save_as_token" type="checkbox"/> <input name="o_payment_save_as_token" type="checkbox"/>
Save my payment details Save my payment details
</label> </label>
@@ -115,14 +115,14 @@
type="radio" type="radio"
t-att-checked="token.id == default_payment_option_id" t-att-checked="token.id == default_payment_option_id"
t-att-data-payment-option-id="token.id" t-att-data-payment-option-id="token.id"
t-att-data-provider="token.provider" t-att-data-provider="token.provider_code"
data-payment-option-type="token"/> data-payment-option-type="token"/>
<!-- === Token name === --> <!-- === Token name === -->
<span class="payment_option_name" t-esc="token.display_name"/> <span class="payment_option_name" t-esc="token.display_name"/>
<!-- === "V" check mark === --> <!-- === "V" check mark === -->
<t t-call="payment.verified_token_checkmark"/> <t t-call="payment.verified_token_checkmark"/>
<!-- === "Unpublished" badge === --> <!-- === "Unpublished" badge === -->
<span t-if="not token.acquirer_id.is_published" class="badge text-bg-danger ms-1"> <span t-if="not token.provider_id.is_published" class="badge text-bg-danger ms-1">
Unpublished Unpublished
</span> </span>
</label> </label>
@@ -132,8 +132,8 @@
name="o_payment_inline_form" name="o_payment_inline_form"
class="card-footer d-none"> class="card-footer d-none">
<t t-set="token_inline_form_xml_id" <t t-set="token_inline_form_xml_id"
t-value="token.sudo().acquirer_id.token_inline_form_view_id.xml_id"/> t-value="token.sudo().provider_id.token_inline_form_view_id.xml_id"/>
<!-- === Inline form content (filled by acquirer) === --> <!-- === Inline form content (filled by provider) === -->
<div t-if="token_inline_form_xml_id" class="clearfix"> <div t-if="token_inline_form_xml_id" class="clearfix">
<t t-call="{{token_inline_form_xml_id}}"> <t t-call="{{token_inline_form_xml_id}}">
<t t-set="token" t-value="token"/> <t t-set="token" t-value="token"/>
@@ -153,7 +153,7 @@
<!-- Manage (token create and deletion) form --> <!-- Manage (token create and deletion) form -->
<template id="manage" name="Payment Manage"> <template id="manage" name="Payment Manage">
<!-- Variables description: <!-- Variables description:
- 'acquirers' - The payment acquirers supporting tokenization - 'providers' - The payment providers supporting tokenization
- 'tokens' - The set of payment tokens of the current partner - 'tokens' - The set of payment tokens of the current partner
- 'default_token_id' - The id of the token that should be pre-selected. Optional - 'default_token_id' - The id of the token that should be pre-selected. Optional
- 'reference_prefix' - The custom prefix to compute the full transaction reference - 'reference_prefix' - The custom prefix to compute the full transaction reference
@@ -176,62 +176,62 @@
t-att-data-assign-token-route="assign_token_route" t-att-data-assign-token-route="assign_token_route"
t-att-data-landing-route="landing_route" t-att-data-landing-route="landing_route"
t-att-data-allow-token-selection="bool(assign_token_route)"> t-att-data-allow-token-selection="bool(assign_token_route)">
<t t-set="acquirer_count" t-value="len(acquirers) if acquirers else 0"/> <t t-set="provider_count" t-value="len(providers) if providers else 0"/>
<t t-set="token_count" t-value="len(tokens) if tokens else 0"/> <t t-set="token_count" t-value="len(tokens) if tokens else 0"/>
<t t-set="no_selectable_token" t-value="token_count == 0 or not assign_token_route"/> <t t-set="no_selectable_token" t-value="token_count == 0 or not assign_token_route"/>
<t t-set="default_payment_option_id" <t t-set="default_payment_option_id"
t-value="default_token_id if default_token_id and token_count > 0 t-value="default_token_id if default_token_id and token_count > 0
else acquirers[0].id if acquirer_count == 1 and no_selectable_token else providers[0].id if provider_count == 1 and no_selectable_token
else None"/> else None"/>
<t t-set="footer_template_id" <t t-set="footer_template_id"
t-value="footer_template_id or 'payment.footer'"/> t-value="footer_template_id or 'payment.footer'"/>
<div class="card"> <div class="card">
<!-- === Acquirers === --> <!-- === Providers === -->
<t t-foreach="acquirers" t-as="acquirer"> <t t-foreach="providers" t-as="provider">
<div name="o_payment_option_card" class="card-body o_payment_option_card"> <div name="o_payment_option_card" class="card-body o_payment_option_card">
<label> <label>
<!-- === Radio button === --> <!-- === Radio button === -->
<!-- Only shown if linked to the only payment option --> <!-- Only shown if linked to the only payment option -->
<input name="o_payment_radio" <input name="o_payment_radio"
type="radio" type="radio"
t-att-checked="acquirer.id == default_payment_option_id" t-att-checked="provider.id == default_payment_option_id"
t-att-class="'' if acquirer_count + token_count > 1 else 'd-none'" t-att-class="'' if provider_count + token_count > 1 else 'd-none'"
t-att-data-payment-option-id="acquirer.id" t-att-data-payment-option-id="provider.id"
t-att-data-provider="acquirer.provider" t-att-data-provider="provider.code"
data-payment-option-type="acquirer"/> data-payment-option-type="provider"/>
<!-- === Acquirer name === --> <!-- === Provider name === -->
<span class="payment_option_name"> <span class="payment_option_name">
<b t-esc="acquirer.display_as or acquirer.name"/> <b t-esc="provider.display_as or provider.name"/>
</span> </span>
<!-- === "Test Mode" badge === --> <!-- === "Test Mode" badge === -->
<span t-if="acquirer.state == 'test'" <span t-if="provider.state == 'test'"
class="rounded-pill text-bg-warning" class="rounded-pill text-bg-warning"
style="margin-left:5px"> style="margin-left:5px">
Test Mode Test Mode
</span> </span>
<!-- === "Unpublished" badge === --> <!-- === "Unpublished" badge === -->
<span t-if="not acquirer.is_published" class="badge text-bg-danger"> <span t-if="not provider.is_published" class="badge text-bg-danger">
Unpublished Unpublished
</span> </span>
</label> </label>
<!-- === Payment icon list === --> <!-- === Payment icon list === -->
<t t-call="payment.icon_list"/> <t t-call="payment.icon_list"/>
<!-- === Help message === --> <!-- === Help message === -->
<div t-if="not is_html_empty(acquirer.pre_msg)" <div t-if="not is_html_empty(provider.pre_msg)"
t-out="acquirer.pre_msg" t-out="provider.pre_msg"
class="text-muted ms-3"/> class="text-muted ms-3"/>
</div> </div>
<!-- === Acquirer inline form === --> <!-- === Provider inline form === -->
<t t-if="acquirer.sudo()._should_build_inline_form(is_validation=True)"> <t t-if="provider.sudo()._should_build_inline_form(is_validation=True)">
<div t-attf-id="o_payment_acquirer_inline_form_{{acquirer.id}}" <div t-attf-id="o_payment_provider_inline_form_{{provider.id}}"
name="o_payment_inline_form" name="o_payment_inline_form"
class="card-footer d-none"> class="card-footer d-none">
<t t-set="inline_form_xml_id" <t t-set="inline_form_xml_id"
t-value="acquirer.sudo().inline_form_view_id.xml_id"/> t-value="provider.sudo().inline_form_view_id.xml_id"/>
<!-- === Inline form content (filled by acquirer) === --> <!-- === Inline form content (filled by provider) === -->
<div t-if="inline_form_xml_id" class="clearfix"> <div t-if="inline_form_xml_id" class="clearfix">
<t t-call="{{inline_form_xml_id}}"> <t t-call="{{inline_form_xml_id}}">
<t t-set="acquirer_id" t-value="acquirer.id"/> <t t-set="provider_id" t-value="provider.id"/>
</t> </t>
</div> </div>
</div> </div>
@@ -248,14 +248,14 @@
t-att-checked="token.id == default_payment_option_id" t-att-checked="token.id == default_payment_option_id"
t-att-class="'' if bool(assign_token_route) else 'd-none'" t-att-class="'' if bool(assign_token_route) else 'd-none'"
t-att-data-payment-option-id="token.id" t-att-data-payment-option-id="token.id"
t-att-data-provider="token.provider" t-att-data-provider="token.provider_code"
data-payment-option-type="token"/> data-payment-option-type="token"/>
<!-- === Token name === --> <!-- === Token name === -->
<span class="payment_option_name" t-esc="token.display_name"/> <span class="payment_option_name" t-esc="token.display_name"/>
<!-- === "V" check mark === --> <!-- === "V" check mark === -->
<t t-call="payment.verified_token_checkmark"/> <t t-call="payment.verified_token_checkmark"/>
<!-- === "Unpublished" badge === --> <!-- === "Unpublished" badge === -->
<span t-if="not token.acquirer_id.is_published and token.env.user._is_internal()" <span t-if="not token.provider_id.is_published and token.env.user._is_internal()"
class="badge text-bg-danger ms-1"> class="badge text-bg-danger ms-1">
Unpublished Unpublished
</span> </span>
@@ -271,8 +271,8 @@
name="o_payment_inline_form" name="o_payment_inline_form"
class="card-footer d-none"> class="card-footer d-none">
<t t-set="token_inline_form_xml_id" <t t-set="token_inline_form_xml_id"
t-value="token.sudo().acquirer_id.token_inline_form_view_id.xml_id"/> t-value="token.sudo().provider_id.token_inline_form_view_id.xml_id"/>
<!-- === Inline form content (filled by acquirer) === --> <!-- === Inline form content (filled by provider) === -->
<div t-if="token_inline_form_xml_id" class="clearfix"> <div t-if="token_inline_form_xml_id" class="clearfix">
<t t-call="{{token_inline_form_xml_id}}"> <t t-call="{{token_inline_form_xml_id}}">
<t t-set="token" t-value="token"/> <t t-set="token" t-value="token"/>
@@ -292,7 +292,7 @@
<!-- Express Checkout form --> <!-- Express Checkout form -->
<template id="express_checkout" name="Payment Express Checkout"> <template id="express_checkout" name="Payment Express Checkout">
<!-- Variables description: <!-- Variables description:
- 'acquirers' - The payment acquirers compatible with the current transaction. - 'providers' - The payment providers compatible with the current transaction.
- 'reference_prefix' - The custom prefix to compute the full transaction reference. - 'reference_prefix' - The custom prefix to compute the full transaction reference.
- 'amount' - The amount to pay. - 'amount' - The amount to pay.
- 'minor_amount' - The amount to pay in the minor units of its currency. - 'minor_amount' - The amount to pay in the minor units of its currency.
@@ -320,12 +320,12 @@
t-att-data-landing-route="landing_route" t-att-data-landing-route="landing_route"
t-att-data-express-route="express_route" t-att-data-express-route="express_route"
t-att-data-shipping-address-update-route="shipping_address_update_route"> t-att-data-shipping-address-update-route="shipping_address_update_route">
<t t-foreach="acquirers_sudo" t-as="acquirer_sudo"> <t t-foreach="providers_sudo" t-as="provider_sudo">
<t t-set="express_checkout_form_xml_id" <t t-set="express_checkout_form_xml_id"
t-value="acquirer_sudo.express_checkout_form_view_id.xml_id"/> t-value="provider_sudo.express_checkout_form_view_id.xml_id"/>
<t t-if="express_checkout_form_xml_id"> <t t-if="express_checkout_form_xml_id">
<t t-call="{{express_checkout_form_xml_id}}"> <t t-call="{{express_checkout_form_xml_id}}">
<t t-set="acquirer_sudo" t-value="acquirer_sudo"/> <t t-set="provider_sudo" t-value="provider_sudo"/>
</t> </t>
</t> </t>
</t> </t>
@@ -339,7 +339,7 @@
<t t-set="MAX_ICONS" t-value="3"/> <t t-set="MAX_ICONS" t-value="3"/>
<!-- === Icons === --> <!-- === Icons === -->
<!-- Only shown if in the first 3 icons --> <!-- Only shown if in the first 3 icons -->
<t t-foreach="acquirer.payment_icon_ids.filtered(lambda r: r.image_payment_form)" t-as="icon"> <t t-foreach="provider.payment_icon_ids.filtered(lambda r: r.image_payment_form)" t-as="icon">
<li t-attf-class="list-inline-item{{'' if (icon_index &lt; MAX_ICONS) else ' d-none'}}"> <li t-attf-class="list-inline-item{{'' if (icon_index &lt; MAX_ICONS) else ' d-none'}}">
<span t-esc="icon.image_payment_form" <span t-esc="icon.image_payment_form"
t-options="{'widget': 'image', 'alt-field': 'name'}" t-options="{'widget': 'image', 'alt-field': 'name'}"
@@ -355,7 +355,7 @@
<span class="float-end more_option text-info"> <span class="float-end more_option text-info">
<a name="o_payment_icon_more" <a name="o_payment_icon_more"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
t-att-title="', '.join([icon.name for icon in acquirer.payment_icon_ids[MAX_ICONS:]])"> t-att-title="', '.join([icon.name for icon in provider.payment_icon_ids[MAX_ICONS:]])">
show more show more
</a> </a>
</span> </span>
@@ -416,19 +416,19 @@
</t> </t>
<t t-elif="tx.state == 'pending'"> <t t-elif="tx.state == 'pending'">
<t t-set="alert_style">warning</t> <t t-set="alert_style">warning</t>
<t t-set="status_message" t-value="tx.acquirer_id.sudo().pending_msg"/> <t t-set="status_message" t-value="tx.provider_id.sudo().pending_msg"/>
</t> </t>
<t t-elif="tx.state == 'authorized'"> <t t-elif="tx.state == 'authorized'">
<t t-set="alert_style">success</t> <t t-set="alert_style">success</t>
<t t-set="status_message" t-value="tx.acquirer_id.sudo().auth_msg"/> <t t-set="status_message" t-value="tx.provider_id.sudo().auth_msg"/>
</t> </t>
<t t-elif="tx.state == 'done'"> <t t-elif="tx.state == 'done'">
<t t-set="alert_style">success</t> <t t-set="alert_style">success</t>
<t t-set="status_message" t-value="tx.acquirer_id.sudo().done_msg"/> <t t-set="status_message" t-value="tx.provider_id.sudo().done_msg"/>
</t> </t>
<t t-elif="tx.state == 'cancel'"> <t t-elif="tx.state == 'cancel'">
<t t-set="alert_style">danger</t> <t t-set="alert_style">danger</t>
<t t-set="status_message" t-value="tx.acquirer_id.sudo().cancel_msg"/> <t t-set="status_message" t-value="tx.provider_id.sudo().cancel_msg"/>
</t> </t>
<t t-elif="tx.state == 'error'"> <t t-elif="tx.state == 'error'">
<t t-set="alert_style">danger</t> <t t-set="alert_style">danger</t>
+5 -5
View File
@@ -20,8 +20,8 @@
<field name="partner_id" /> <field name="partner_id" />
</group> </group>
<group name="technical_information"> <group name="technical_information">
<field name="acquirer_id"/> <field name="provider_id"/>
<field name="acquirer_ref"/> <field name="provider_ref"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
</group> </group>
</sheet> </sheet>
@@ -36,8 +36,8 @@
<tree string="Payment Tokens"> <tree string="Payment Tokens">
<field name="payment_details"/> <field name="payment_details"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="acquirer_id" readonly="1"/> <field name="provider_id" readonly="1"/>
<field name="acquirer_ref" readonly="1"/> <field name="provider_ref" readonly="1"/>
<field name="company_id" groups="base.group_multi_company" optional="show"/> <field name="company_id" groups="base.group_multi_company" optional="show"/>
</tree> </tree>
</field> </field>
@@ -52,7 +52,7 @@
<separator/> <separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/> <filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group expand="1" string="Group By"> <group expand="1" string="Group By">
<filter string="Acquirer" name="acquirer_id" context="{'group_by': 'acquirer_id'}"/> <filter string="Provider" name="provider_id" context="{'group_by': 'provider_id'}"/>
<filter string="Partner" name="partner_id" context="{'group_by': 'partner_id'}"/> <filter string="Partner" name="partner_id" context="{'group_by': 'partner_id'}"/>
<filter string="Company" name="company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/> <filter string="Company" name="company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
</group> </group>
@@ -30,11 +30,11 @@
<field name="amount"/> <field name="amount"/>
<field name="fees" attrs="{'invisible': [('fees', '=', 0.0)]}"/> <field name="fees" attrs="{'invisible': [('fees', '=', 0.0)]}"/>
<field name="currency_id" invisible="1"/> <field name="currency_id" invisible="1"/>
<field name="acquirer_id"/> <field name="provider_id"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
<!-- Used by some acquirer-specific views --> <!-- Used by some provider-specific views -->
<field name="provider" invisible="1"/> <field name="provider_code" invisible="1"/>
<field name="acquirer_reference"/> <field name="provider_reference"/>
<field name="token_id" attrs="{'invisible': [('token_id', '=', False)]}"/> <field name="token_id" attrs="{'invisible': [('token_id', '=', False)]}"/>
<field name="create_date"/> <field name="create_date"/>
<field name="last_state_change"/> <field name="last_state_change"/>
@@ -69,7 +69,7 @@
<tree string="Payment Transactions" create="false"> <tree string="Payment Transactions" create="false">
<field name="reference"/> <field name="reference"/>
<field name="create_date"/> <field name="create_date"/>
<field name="acquirer_id"/> <field name="provider_id"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="partner_name"/> <field name="partner_name"/>
<!-- Needed to display the currency of the amounts --> <!-- Needed to display the currency of the amounts -->
@@ -117,11 +117,11 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<search> <search>
<field name="reference"/> <field name="reference"/>
<field name="acquirer_id"/> <field name="provider_id"/>
<field name="partner_id"/> <field name="partner_id"/>
<field name="partner_name"/> <field name="partner_name"/>
<group expand="1" string="Group By"> <group expand="1" string="Group By">
<filter string="Acquirer" name="acquirer_id" context="{'group_by': 'acquirer_id'}"/> <filter string="Provider" name="provider_id" context="{'group_by': 'provider_id'}"/>
<filter string="Partner" name="partner_id" context="{'group_by': 'partner_id'}"/> <filter string="Partner" name="partner_id" context="{'group_by': 'partner_id'}"/>
<filter string="Status" name="state" context="{'group_by': 'state'}"/> <filter string="Status" name="state" context="{'group_by': 'state'}"/>
<filter string="Company" name="company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/> <filter string="Company" name="company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
+29 -29
View File
@@ -35,22 +35,22 @@ class PaymentLinkWizard(models.TransientModel):
description = fields.Char("Payment Ref") description = fields.Char("Payment Ref")
link = fields.Char(string="Payment Link", compute='_compute_link') link = fields.Char(string="Payment Link", compute='_compute_link')
company_id = fields.Many2one('res.company', compute='_compute_company_id') company_id = fields.Many2one('res.company', compute='_compute_company_id')
available_acquirer_ids = fields.Many2many( available_provider_ids = fields.Many2many(
comodel_name='payment.acquirer', comodel_name='payment.provider',
string="Payment Acquirers Available", string="Payment Providers Available",
compute='_compute_available_acquirer_ids', compute='_compute_available_provider_ids',
compute_sudo=True, compute_sudo=True,
) )
has_multiple_acquirers = fields.Boolean( has_multiple_providers = fields.Boolean(
string="Has Multiple Acquirers", string="Has Multiple Providers",
compute='_compute_has_multiple_acquirers', compute='_compute_has_multiple_providers',
) )
payment_acquirer_selection = fields.Selection( payment_provider_selection = fields.Selection(
string="Allow Payment Acquirer", string="Allow Payment Provider",
help="If a specific payment acquirer is selected, customers will only be allowed to pay " help="If a specific payment provider is selected, customers will only be allowed to pay "
"via this one. If 'All' is selected, customers can pay via any available payment " "via this one. If 'All' is selected, customers can pay via any available payment "
"acquirer.", "provider.",
selection='_selection_payment_acquirer_selection', selection='_selection_payment_provider_selection',
default='all', default='all',
required=True, required=True,
) )
@@ -69,9 +69,9 @@ class PaymentLinkWizard(models.TransientModel):
link.company_id = record.company_id if 'company_id' in record else False link.company_id = record.company_id if 'company_id' in record else False
@api.depends('company_id', 'partner_id', 'currency_id') @api.depends('company_id', 'partner_id', 'currency_id')
def _compute_available_acquirer_ids(self): def _compute_available_provider_ids(self):
for link in self: for link in self:
link.available_acquirer_ids = link._get_payment_acquirer_available( link.available_provider_ids = link._get_payment_provider_available(
res_model=link.res_model, res_model=link.res_model,
res_id=link.res_id, res_id=link.res_id,
company_id=link.company_id.id, company_id=link.company_id.id,
@@ -80,9 +80,9 @@ class PaymentLinkWizard(models.TransientModel):
currency_id=link.currency_id.id, currency_id=link.currency_id.id,
) )
def _selection_payment_acquirer_selection(self): def _selection_payment_provider_selection(self):
""" Specify available acquirers in the selection field. """ Specify available providers in the selection field.
:return: The selection list of available acquirers. :return: The selection list of available providers.
:rtype: list[tuple] :rtype: list[tuple]
""" """
defaults = self.default_get(['res_model', 'res_id']) defaults = self.default_get(['res_model', 'res_id'])
@@ -96,7 +96,7 @@ class PaymentLinkWizard(models.TransientModel):
partner_id = related_document.partner_id partner_id = related_document.partner_id
currency_id = related_document.currency_id currency_id = related_document.currency_id
selection.extend( selection.extend(
self._get_payment_acquirer_available( self._get_payment_provider_available(
res_model=res_model, res_model=res_model,
res_id=res_id, res_id=res_id,
company_id=company_id.id, company_id=company_id.id,
@@ -107,18 +107,18 @@ class PaymentLinkWizard(models.TransientModel):
) )
return selection return selection
def _get_payment_acquirer_available(self, **kwargs): def _get_payment_provider_available(self, **kwargs):
""" Select and return the acquirers matching the criteria. """ Select and return the providers matching the criteria.
:return: The compatible acquirers :return: The compatible providers
:rtype: recordset of `payment.acquirer` :rtype: recordset of `payment.provider`
""" """
return self.env['payment.acquirer'].sudo()._get_compatible_acquirers(**kwargs) return self.env['payment.provider'].sudo()._get_compatible_providers(**kwargs)
@api.depends('available_acquirer_ids') @api.depends('available_provider_ids')
def _compute_has_multiple_acquirers(self): def _compute_has_multiple_providers(self):
for link in self: for link in self:
link.has_multiple_acquirers = len(link.available_acquirer_ids) > 1 link.has_multiple_providers = len(link.available_provider_ids) > 1
def _get_access_token(self): def _get_access_token(self):
self.ensure_one() self.ensure_one()
@@ -128,7 +128,7 @@ class PaymentLinkWizard(models.TransientModel):
@api.depends( @api.depends(
'description', 'amount', 'currency_id', 'partner_id', 'company_id', 'description', 'amount', 'currency_id', 'partner_id', 'company_id',
'payment_acquirer_selection', 'payment_provider_selection',
) )
def _compute_link(self): def _compute_link(self):
for payment_link in self: for payment_link in self:
@@ -140,8 +140,8 @@ class PaymentLinkWizard(models.TransientModel):
'access_token': self._get_access_token(), 'access_token': self._get_access_token(),
**self._get_additional_link_values(), **self._get_additional_link_values(),
} }
if payment_link.payment_acquirer_selection != 'all': if payment_link.payment_provider_selection != 'all':
url_params['acquirer_id'] = str(payment_link.payment_acquirer_selection) url_params['provider_id'] = str(payment_link.payment_provider_selection)
payment_link.link = f'{base_url}/payment/pay?{urls.url_encode(url_params)}' payment_link.link = f'{base_url}/payment/pay?{urls.url_encode(url_params)}'
def _get_additional_link_values(self): def _get_additional_link_values(self):
@@ -13,20 +13,20 @@
<field name="partner_id" invisible="1"/> <field name="partner_id" invisible="1"/>
<field name="partner_email" invisible="1"/> <field name="partner_email" invisible="1"/>
<field name="amount_max" invisible="1"/> <field name="amount_max" invisible="1"/>
<field name="available_acquirer_ids" invisible="1"/> <field name="available_provider_ids" invisible="1"/>
<field name="has_multiple_acquirers" invisible="1"/> <field name="has_multiple_providers" invisible="1"/>
<field name="description"/> <field name="description"/>
<field name="amount"/> <field name="amount"/>
<field name="currency_id" invisible="1"/> <field name="currency_id" invisible="1"/>
<field name="payment_acquirer_selection" <field name="payment_provider_selection"
attrs="{'invisible':[('has_multiple_acquirers', '=', False)]}"/> attrs="{'invisible':[('has_multiple_providers', '=', False)]}"/>
</group> </group>
</group> </group>
<group> <group>
<field name="link" readonly="1" widget="CopyClipboardChar"/> <field name="link" readonly="1" widget="CopyClipboardChar"/>
</group> </group>
<group attrs="{'invisible':[('partner_email', '!=', False)]}"> <group attrs="{'invisible':[('partner_email', '!=', False)]}">
<p class="alert alert-warning fw-bold" role="alert">This partner has no email, which may cause issues with some payment acquirers. Setting an email for this partner is advised.</p> <p class="alert alert-warning fw-bold" role="alert">This partner has no email, which may cause issues with some payment providers. Setting an email for this partner is advised.</p>
</group> </group>
<footer> <footer>
<button string="Close" class="btn-primary" special="cancel" data-hotkey="z"/> <button string="Close" class="btn-primary" special="cancel" data-hotkey="z"/>
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<record id="payment_acquirer_onboarding_wizard_form" model="ir.ui.view"> <record id="payment_provider_onboarding_wizard_form" model="ir.ui.view">
<field name="name">payment.acquirer.onboarding.wizard.form</field> <field name="name">payment.provider.onboarding.wizard.form</field>
<field name="model">payment.acquirer.onboarding.wizard</field> <field name="model">payment.provider.onboarding.wizard</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Choose a payment method" class="o_onboarding_payment_acquirer_wizard"> <form string="Choose a payment method" class="o_onboarding_payment_provider_wizard">
<div class="container"> <div class="container">
<div class="row align-items-start"> <div class="row align-items-start">
<div class="col col-4" name="left-column"> <div class="col col-4" name="left-column">
@@ -25,7 +25,7 @@
<span>Start selling directly without an account; an email will be sent by Paypal to create your new account and collect your payments.</span> <span>Start selling directly without an account; an email will be sent by Paypal to create your new account and collect your payments.</span>
</p> </p>
<p attrs="{'invisible': [('paypal_user_type', '=', 'new_user')]}"> <p attrs="{'invisible': [('paypal_user_type', '=', 'new_user')]}">
<a href="https://www.odoo.com/documentation/master/applications/finance/payment_acquirers/paypal.html" target="_blank"> <a href="https://www.odoo.com/documentation/master/applications/finance/payment_providers/paypal.html" target="_blank">
<span><i class="fa fa-arrow-right"/> How to configure your PayPal account</span> <span><i class="fa fa-arrow-right"/> How to configure your PayPal account</span>
</a> </a>
</p> </p>
@@ -5,26 +5,26 @@ from odoo.exceptions import UserError
class PaymentWizard(models.TransientModel): class PaymentWizard(models.TransientModel):
_name = 'payment.acquirer.onboarding.wizard' _name = 'payment.provider.onboarding.wizard'
_description = 'Payment acquire onboarding wizard' _description = 'Payment provider onboarding wizard'
payment_method = fields.Selection([ payment_method = fields.Selection([
('stripe', "Credit & Debit card (via Stripe)"), ('stripe', "Credit & Debit card (via Stripe)"),
('paypal', "PayPal"), ('paypal', "PayPal"),
('manual', "Custom payment instructions"), ('manual', "Custom payment instructions"),
], string="Payment Method", default=lambda self: self._get_default_payment_acquirer_onboarding_value('payment_method')) ], string="Payment Method", default=lambda self: self._get_default_payment_provider_onboarding_value('payment_method'))
paypal_user_type = fields.Selection([ paypal_user_type = fields.Selection([
('new_user', "I don't have a Paypal account"), ('new_user', "I don't have a Paypal account"),
('existing_user', 'I have a Paypal account')], string="Paypal User Type", default='new_user') ('existing_user', 'I have a Paypal account')], string="Paypal User Type", default='new_user')
paypal_email_account = fields.Char("Email", default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_email_account')) paypal_email_account = fields.Char("Email", default=lambda self: self._get_default_payment_provider_onboarding_value('paypal_email_account'))
paypal_seller_account = fields.Char("Merchant Account ID", default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_seller_account')) paypal_seller_account = fields.Char("Merchant Account ID", default=lambda self: self._get_default_payment_provider_onboarding_value('paypal_seller_account'))
paypal_pdt_token = fields.Char("PDT Identity Token", default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_pdt_token')) paypal_pdt_token = fields.Char("PDT Identity Token", default=lambda self: self._get_default_payment_provider_onboarding_value('paypal_pdt_token'))
# Account-specific logic. It's kept here rather than moved in `account_payment` as it's not used by `account` module. # Account-specific logic. It's kept here rather than moved in `account_payment` as it's not used by `account` module.
manual_name = fields.Char("Method", default=lambda self: self._get_default_payment_acquirer_onboarding_value('manual_name')) manual_name = fields.Char("Method", default=lambda self: self._get_default_payment_provider_onboarding_value('manual_name'))
journal_name = fields.Char("Bank Name", default=lambda self: self._get_default_payment_acquirer_onboarding_value('journal_name')) journal_name = fields.Char("Bank Name", default=lambda self: self._get_default_payment_provider_onboarding_value('journal_name'))
acc_number = fields.Char("Account Number", default=lambda self: self._get_default_payment_acquirer_onboarding_value('acc_number')) acc_number = fields.Char("Account Number", default=lambda self: self._get_default_payment_provider_onboarding_value('acc_number'))
manual_post_msg = fields.Html("Payment Instructions") manual_post_msg = fields.Html("Payment Instructions")
_data_fetched = fields.Boolean(store=False) _data_fetched = fields.Boolean(store=False)
@@ -38,25 +38,25 @@ class PaymentWizard(models.TransientModel):
self.env.company.name self.env.company.name
) )
_payment_acquirer_onboarding_cache = {} _payment_provider_onboarding_cache = {}
def _get_manual_payment_acquirer(self, env=None): def _get_manual_payment_provider(self, env=None):
if env is None: if env is None:
env = self.env env = self.env
module_id = env.ref('base.module_payment_custom').id module_id = env.ref('base.module_payment_custom').id
return env['payment.acquirer'].search([('module_id', '=', module_id), return env['payment.provider'].search([('module_id', '=', module_id),
('company_id', '=', env.company.id)], limit=1) ('company_id', '=', env.company.id)], limit=1)
def _get_default_payment_acquirer_onboarding_value(self, key): def _get_default_payment_provider_onboarding_value(self, key):
if not self.env.is_admin(): if not self.env.is_admin():
raise UserError(_("Only administrators can access this data.")) raise UserError(_("Only administrators can access this data."))
if self._data_fetched: if self._data_fetched:
return self._payment_acquirer_onboarding_cache.get(key, '') return self._payment_provider_onboarding_cache.get(key, '')
self._data_fetched = True self._data_fetched = True
self._payment_acquirer_onboarding_cache['payment_method'] = self.env.company.payment_onboarding_payment_method self._payment_provider_onboarding_cache['payment_method'] = self.env.company.payment_onboarding_payment_method
installed_modules = self.env['ir.module.module'].sudo().search([ installed_modules = self.env['ir.module.module'].sudo().search([
('name', 'in', ('payment_paypal', 'payment_stripe')), ('name', 'in', ('payment_paypal', 'payment_stripe')),
@@ -64,23 +64,23 @@ class PaymentWizard(models.TransientModel):
]).mapped('name') ]).mapped('name')
if 'payment_paypal' in installed_modules: if 'payment_paypal' in installed_modules:
acquirer = self.env.ref('payment.payment_acquirer_paypal') provider = self.env.ref('payment.payment_provider_paypal')
self._payment_acquirer_onboarding_cache['paypal_email_account'] = acquirer['paypal_email_account'] or self.env.user.email or '' self._payment_provider_onboarding_cache['paypal_email_account'] = provider['paypal_email_account'] or self.env.user.email or ''
self._payment_acquirer_onboarding_cache['paypal_seller_account'] = acquirer['paypal_seller_account'] self._payment_provider_onboarding_cache['paypal_seller_account'] = provider['paypal_seller_account']
self._payment_acquirer_onboarding_cache['paypal_pdt_token'] = acquirer['paypal_pdt_token'] self._payment_provider_onboarding_cache['paypal_pdt_token'] = provider['paypal_pdt_token']
manual_payment = self._get_manual_payment_acquirer() manual_payment = self._get_manual_payment_provider()
journal = manual_payment.journal_id journal = manual_payment.journal_id
self._payment_acquirer_onboarding_cache['manual_name'] = manual_payment['name'] self._payment_provider_onboarding_cache['manual_name'] = manual_payment['name']
self._payment_acquirer_onboarding_cache['manual_post_msg'] = manual_payment['pending_msg'] self._payment_provider_onboarding_cache['manual_post_msg'] = manual_payment['pending_msg']
self._payment_acquirer_onboarding_cache['journal_name'] = journal.name if journal.name != "Bank" else "" self._payment_provider_onboarding_cache['journal_name'] = journal.name if journal.name != "Bank" else ""
self._payment_acquirer_onboarding_cache['acc_number'] = journal.bank_acc_number self._payment_provider_onboarding_cache['acc_number'] = journal.bank_acc_number
return self._payment_acquirer_onboarding_cache.get(key, '') return self._payment_provider_onboarding_cache.get(key, '')
def add_payment_methods(self): def add_payment_methods(self):
""" Install required payment acquirers, configure them and mark the """ Install required payment providers, configure them and mark the
onboarding step as done.""" onboarding step as done."""
payment_method = self.payment_method payment_method = self.payment_method
@@ -94,24 +94,24 @@ class PaymentWizard(models.TransientModel):
new_env = api.Environment(self.env.cr, self.env.uid, self.env.context) new_env = api.Environment(self.env.cr, self.env.uid, self.env.context)
if self.payment_method == 'paypal': if self.payment_method == 'paypal':
new_env.ref('payment.payment_acquirer_paypal').write({ new_env.ref('payment.payment_provider_paypal').write({
'paypal_email_account': self.paypal_email_account, 'paypal_email_account': self.paypal_email_account,
'paypal_seller_account': self.paypal_seller_account, 'paypal_seller_account': self.paypal_seller_account,
'paypal_pdt_token': self.paypal_pdt_token, 'paypal_pdt_token': self.paypal_pdt_token,
'state': 'enabled', 'state': 'enabled',
}) })
elif self.payment_method == 'manual': elif self.payment_method == 'manual':
manual_acquirer = self._get_manual_payment_acquirer(new_env) manual_provider = self._get_manual_payment_provider(new_env)
if not manual_acquirer: if not manual_provider:
raise UserError(_( raise UserError(_(
'No manual payment method could be found for this company. ' 'No manual payment method could be found for this company. '
'Please create one from the Payment Acquirer menu.' 'Please create one from the Payment Provider menu.'
)) ))
manual_acquirer.name = self.manual_name manual_provider.name = self.manual_name
manual_acquirer.pending_msg = self.manual_post_msg manual_provider.pending_msg = self.manual_post_msg
manual_acquirer.state = 'enabled' manual_provider.state = 'enabled'
journal = manual_acquirer.journal_id journal = manual_provider.journal_id
if journal: if journal:
journal.name = self.journal_name journal.name = self.journal_name
journal.bank_acc_number = self.acc_number journal.bank_acc_number = self.acc_number
@@ -126,14 +126,14 @@ class PaymentWizard(models.TransientModel):
return self._start_stripe_onboarding() return self._start_stripe_onboarding()
# the user clicked `apply` and not cancel so we can assume this step is done. # the user clicked `apply` and not cancel so we can assume this step is done.
self._set_payment_acquirer_onboarding_step_done() self._set_payment_provider_onboarding_step_done()
return {'type': 'ir.actions.act_window_close'} return {'type': 'ir.actions.act_window_close'}
def _set_payment_acquirer_onboarding_step_done(self): def _set_payment_provider_onboarding_step_done(self):
self.env.company.sudo().set_onboarding_step_done('payment_acquirer_onboarding_state') self.env.company.sudo().set_onboarding_step_done('payment_provider_onboarding_state')
def _start_stripe_onboarding(self): def _start_stripe_onboarding(self):
""" Start Stripe Connect onboarding. """ """ Start Stripe Connect onboarding. """
menu = self.env.ref('account_payment.payment_acquirer_menu', False) menu = self.env.ref('account_payment.payment_provider_menu', False)
menu_id = menu and menu.id # Only set if `account_payment` is installed. menu_id = menu and menu.id # Only set if `account_payment` is installed.
return self.env.company._run_payment_onboarding_step(menu_id) return self.env.company._run_payment_onboarding_step(menu_id)
+2 -2
View File
@@ -3,7 +3,7 @@
from . import controllers from . import controllers
from . import models from . import models
from odoo.addons.payment import setup_provider, reset_payment_acquirer from odoo.addons.payment import setup_provider, reset_payment_provider
def post_init_hook(cr, registry): def post_init_hook(cr, registry):
@@ -11,4 +11,4 @@ def post_init_hook(cr, registry):
def uninstall_hook(cr, registry): def uninstall_hook(cr, registry):
reset_payment_acquirer(cr, registry, 'adyen') reset_payment_provider(cr, registry, 'adyen')
+5 -5
View File
@@ -1,18 +1,18 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
{ {
'name': 'Adyen Payment Acquirer', 'name': 'Adyen Payment Provider',
'version': '2.0', 'version': '2.0',
'category': 'Accounting/Payment Acquirers', 'category': 'Accounting/Payment Providers',
'sequence': 340, 'sequence': 340,
'summary': 'Payment Acquirer: Adyen Implementation', 'summary': 'Payment Provider: Adyen Implementation',
'description': """Adyen Payment Acquirer""", 'description': """Adyen Payment Provider""",
'depends': ['payment'], 'depends': ['payment'],
'data': [ 'data': [
'views/payment_adyen_templates.xml', 'views/payment_adyen_templates.xml',
'views/payment_views.xml', 'views/payment_views.xml',
'views/payment_templates.xml', # Only load the SDK on pages with a payment form. 'views/payment_templates.xml', # Only load the SDK on pages with a payment form.
'data/payment_acquirer_data.xml', # Depends on views/payment_adyen_templates.xml 'data/payment_provider_data.xml', # Depends on views/payment_adyen_templates.xml
], ],
'application': True, 'application': True,
'post_init_hook': 'post_init_hook', 'post_init_hook': 'post_init_hook',
+31 -31
View File
@@ -24,32 +24,32 @@ class AdyenController(http.Controller):
_webhook_url = '/payment/adyen/notification' _webhook_url = '/payment/adyen/notification'
@http.route('/payment/adyen/acquirer_info', type='json', auth='public') @http.route('/payment/adyen/provider_info', type='json', auth='public')
def adyen_acquirer_info(self, acquirer_id): def adyen_provider_info(self, provider_id):
""" Return public information on the acquirer. """ Return public information on the provider.
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id :param int provider_id: The provider handling the transaction, as a `payment.provider` id
:return: Public information on the acquirer, namely: the state and client key :return: Public information on the provider, namely: the state and client key
:rtype: str :rtype: str
""" """
acquirer_sudo = request.env['payment.acquirer'].sudo().browse(acquirer_id).exists() provider_sudo = request.env['payment.provider'].sudo().browse(provider_id).exists()
return { return {
'state': acquirer_sudo.state, 'state': provider_sudo.state,
'client_key': acquirer_sudo.adyen_client_key, 'client_key': provider_sudo.adyen_client_key,
} }
@http.route('/payment/adyen/payment_methods', type='json', auth='public') @http.route('/payment/adyen/payment_methods', type='json', auth='public')
def adyen_payment_methods(self, acquirer_id, amount=None, currency_id=None, partner_id=None): def adyen_payment_methods(self, provider_id, amount=None, currency_id=None, partner_id=None):
""" Query the available payment methods based on the transaction context. """ Query the available payment methods based on the transaction context.
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id :param int provider_id: The provider handling the transaction, as a `payment.provider` id
:param float amount: The transaction amount :param float amount: The transaction amount
:param int currency_id: The transaction currency, as a `res.currency` id :param int currency_id: The transaction currency, as a `res.currency` id
:param int partner_id: The partner making the transaction, as a `res.partner` id :param int partner_id: The partner making the transaction, as a `res.partner` id
:return: The JSON-formatted content of the response :return: The JSON-formatted content of the response
:rtype: dict :rtype: dict
""" """
acquirer_sudo = request.env['payment.acquirer'].sudo().browse(acquirer_id) provider_sudo = request.env['payment.provider'].sudo().browse(provider_id)
currency = request.env['res.currency'].browse(currency_id) currency = request.env['res.currency'].browse(currency_id)
currency_code = currency_id and currency.name currency_code = currency_id and currency.name
converted_amount = amount and currency_code and payment_utils.to_minor_currency_units( converted_amount = amount and currency_code and payment_utils.to_minor_currency_units(
@@ -64,14 +64,14 @@ class AdyenController(http.Controller):
lang_code = (request.context.get('lang') or 'en-US').replace('_', '-') lang_code = (request.context.get('lang') or 'en-US').replace('_', '-')
shopper_reference = partner_sudo and f'ODOO_PARTNER_{partner_sudo.id}' shopper_reference = partner_sudo and f'ODOO_PARTNER_{partner_sudo.id}'
data = { data = {
'merchantAccount': acquirer_sudo.adyen_merchant_account, 'merchantAccount': provider_sudo.adyen_merchant_account,
'amount': converted_amount, 'amount': converted_amount,
'countryCode': partner_sudo.country_id.code or None, # ISO 3166-1 alpha-2 (e.g.: 'BE') 'countryCode': partner_sudo.country_id.code or None, # ISO 3166-1 alpha-2 (e.g.: 'BE')
'shopperLocale': lang_code, # IETF language tag (e.g.: 'fr-BE') 'shopperLocale': lang_code, # IETF language tag (e.g.: 'fr-BE')
'shopperReference': shopper_reference, 'shopperReference': shopper_reference,
'channel': 'Web', 'channel': 'Web',
} }
response_content = acquirer_sudo._adyen_make_request( response_content = provider_sudo._adyen_make_request(
url_field_name='adyen_checkout_api_url', url_field_name='adyen_checkout_api_url',
endpoint='/paymentMethods', endpoint='/paymentMethods',
payload=data, payload=data,
@@ -82,12 +82,12 @@ class AdyenController(http.Controller):
@http.route('/payment/adyen/payments', type='json', auth='public') @http.route('/payment/adyen/payments', type='json', auth='public')
def adyen_payments( def adyen_payments(
self, acquirer_id, reference, converted_amount, currency_id, partner_id, payment_method, self, provider_id, reference, converted_amount, currency_id, partner_id, payment_method,
access_token, browser_info=None access_token, browser_info=None
): ):
""" Make a payment request and handle the notification data. """ Make a payment request and handle the notification data.
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id :param int provider_id: The provider handling the transaction, as a `payment.provider` id
:param str reference: The reference of the transaction :param str reference: The reference of the transaction
:param int converted_amount: The amount of the transaction in minor units of the currency :param int converted_amount: The amount of the transaction in minor units of the currency
:param int currency_id: The currency of the transaction, as a `res.currency` id :param int currency_id: The currency of the transaction, as a `res.currency` id
@@ -106,17 +106,17 @@ class AdyenController(http.Controller):
raise ValidationError("Adyen: " + _("Received tampered payment request data.")) raise ValidationError("Adyen: " + _("Received tampered payment request data."))
# Prepare the payment request to Adyen # Prepare the payment request to Adyen
acquirer_sudo = request.env['payment.acquirer'].sudo().browse(acquirer_id).exists() provider_sudo = request.env['payment.provider'].sudo().browse(provider_id).exists()
tx_sudo = request.env['payment.transaction'].sudo().search([('reference', '=', reference)]) tx_sudo = request.env['payment.transaction'].sudo().search([('reference', '=', reference)])
data = { data = {
'merchantAccount': acquirer_sudo.adyen_merchant_account, 'merchantAccount': provider_sudo.adyen_merchant_account,
'amount': { 'amount': {
'value': converted_amount, 'value': converted_amount,
'currency': request.env['res.currency'].browse(currency_id).name, # ISO 4217 'currency': request.env['res.currency'].browse(currency_id).name, # ISO 4217
}, },
'reference': reference, 'reference': reference,
'paymentMethod': payment_method, 'paymentMethod': payment_method,
'shopperReference': acquirer_sudo._adyen_compute_shopper_reference(partner_id), 'shopperReference': provider_sudo._adyen_compute_shopper_reference(partner_id),
'recurringProcessingModel': 'CardOnFile', # Most susceptible to trigger a 3DS check 'recurringProcessingModel': 'CardOnFile', # Most susceptible to trigger a 3DS check
'shopperIP': payment_utils.get_customer_ip_address(), 'shopperIP': payment_utils.get_customer_ip_address(),
'shopperInteraction': 'Ecommerce', 'shopperInteraction': 'Ecommerce',
@@ -125,10 +125,10 @@ class AdyenController(http.Controller):
'allow3DS2': True 'allow3DS2': True
}, },
'channel': 'web', # Required to support 3DS 'channel': 'web', # Required to support 3DS
'origin': acquirer_sudo.get_base_url(), # Required to support 3DS 'origin': provider_sudo.get_base_url(), # Required to support 3DS
'browserInfo': browser_info, # Required to support 3DS 'browserInfo': browser_info, # Required to support 3DS
'returnUrl': urls.url_join( 'returnUrl': urls.url_join(
acquirer_sudo.get_base_url(), provider_sudo.get_base_url(),
# Include the reference in the return url to be able to match it after redirection. # Include the reference in the return url to be able to match it after redirection.
# The key 'merchantReference' is chosen on purpose to be the same as that returned # The key 'merchantReference' is chosen on purpose to be the same as that returned
# by the /payments endpoint of Adyen. # by the /payments endpoint of Adyen.
@@ -136,18 +136,18 @@ class AdyenController(http.Controller):
), ),
} }
# Force the capture delay on Adyen side if the acquirer is not configured for capturing # Force the capture delay on Adyen side if the provider is not configured for capturing
# payments manually. This is necessary because it's not possible to distinguish # payments manually. This is necessary because it's not possible to distinguish
# 'AUTHORISATION' events sent by Adyen with the merchant account's capture delay set to # 'AUTHORISATION' events sent by Adyen with the merchant account's capture delay set to
# 'manual' from events with the capture delay set to 'immediate' or a number of hours. If # 'manual' from events with the capture delay set to 'immediate' or a number of hours. If
# the merchant account is configured to capture payments with a delay but the acquirer is # the merchant account is configured to capture payments with a delay but the provider is
# not, we force the immediate capture to avoid considering authorized transactions as # not, we force the immediate capture to avoid considering authorized transactions as
# captured on Odoo. # captured on Odoo.
if not acquirer_sudo.capture_manually: if not provider_sudo.capture_manually:
data.update(captureDelayHours=0) data.update(captureDelayHours=0)
# Make the payment request to Adyen # Make the payment request to Adyen
response_content = acquirer_sudo._adyen_make_request( response_content = provider_sudo._adyen_make_request(
url_field_name='adyen_checkout_api_url', url_field_name='adyen_checkout_api_url',
endpoint='/payments', endpoint='/payments',
payload=data, payload=data,
@@ -165,21 +165,21 @@ class AdyenController(http.Controller):
return response_content return response_content
@http.route('/payment/adyen/payment_details', type='json', auth='public') @http.route('/payment/adyen/payment_details', type='json', auth='public')
def adyen_payment_details(self, acquirer_id, reference, payment_details): def adyen_payment_details(self, provider_id, reference, payment_details):
""" Submit the details of the additional actions and handle the notification data. """ Submit the details of the additional actions and handle the notification data.
The additional actions can have been performed both from the inline form or during a The additional actions can have been performed both from the inline form or during a
redirection. redirection.
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id :param int provider_id: The provider handling the transaction, as a `payment.provider` id
:param str reference: The reference of the transaction :param str reference: The reference of the transaction
:param dict payment_details: The details of the additional actions performed for the payment :param dict payment_details: The details of the additional actions performed for the payment
:return: The JSON-formatted content of the response :return: The JSON-formatted content of the response
:rtype: dict :rtype: dict
""" """
# Make the payment details request to Adyen # Make the payment details request to Adyen
acquirer_sudo = request.env['payment.acquirer'].browse(acquirer_id).sudo() provider_sudo = request.env['payment.provider'].browse(provider_id).sudo()
response_content = acquirer_sudo._adyen_make_request( response_content = provider_sudo._adyen_make_request(
url_field_name='adyen_checkout_api_url', url_field_name='adyen_checkout_api_url',
endpoint='/payments/details', endpoint='/payments/details',
payload=payment_details, payload=payment_details,
@@ -229,7 +229,7 @@ class AdyenController(http.Controller):
tx_sudo.reference, pprint.pformat(data) tx_sudo.reference, pprint.pformat(data)
) )
self.adyen_payment_details( self.adyen_payment_details(
tx_sudo.acquirer_id.id, tx_sudo.provider_id.id,
data['merchantReference'], data['merchantReference'],
{ {
'details': { 'details': {
@@ -302,7 +302,7 @@ class AdyenController(http.Controller):
raise Forbidden() raise Forbidden()
# Compare the received signature with the expected signature computed from the payload # Compare the received signature with the expected signature computed from the payload
hmac_key = tx_sudo.acquirer_id.adyen_hmac_key hmac_key = tx_sudo.provider_id.adyen_hmac_key
expected_signature = AdyenController._compute_signature(notification_data, hmac_key) expected_signature = AdyenController._compute_signature(notification_data, hmac_key)
if not hmac.compare_digest(received_signature, expected_signature): if not hmac.compare_digest(received_signature, expected_signature):
_logger.warning("received notification with invalid signature") _logger.warning("received notification with invalid signature")
@@ -315,7 +315,7 @@ class AdyenController(http.Controller):
See https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures See https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures
:param dict payload: The notification payload :param dict payload: The notification payload
:param str hmac_key: The HMAC key of the acquirer handling the transaction :param str hmac_key: The HMAC key of the provider handling the transaction
:return: The computed signature :return: The computed signature
:rtype: str :rtype: str
""" """
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1"> <odoo noupdate="1">
<record id="payment.payment_acquirer_adyen" model="payment.acquirer"> <record id="payment.payment_provider_adyen" model="payment.provider">
<field name="provider">adyen</field> <field name="code">adyen</field>
<field name="inline_form_view_id" ref="inline_form"/> <field name="inline_form_view_id" ref="inline_form"/>
<field name="allow_tokenization">True</field> <field name="allow_tokenization">True</field>
</record> </record>
+1 -1
View File
@@ -1,5 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment_acquirer from . import payment_provider
from . import payment_token from . import payment_token
from . import payment_transaction from . import payment_transaction
@@ -13,14 +13,14 @@ from odoo.addons.payment_adyen.const import API_ENDPOINT_VERSIONS
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class PaymentAcquirer(models.Model): class PaymentProvider(models.Model):
_inherit = 'payment.acquirer' _inherit = 'payment.provider'
provider = fields.Selection( code = fields.Selection(
selection_add=[('adyen', "Adyen")], ondelete={'adyen': 'set default'}) selection_add=[('adyen', "Adyen")], ondelete={'adyen': 'set default'})
adyen_merchant_account = fields.Char( adyen_merchant_account = fields.Char(
string="Merchant Account", string="Merchant Account",
help="The code of the merchant account to use with this acquirer", help="The code of the merchant account to use with this provider",
required_if_provider='adyen', groups='base.group_system') required_if_provider='adyen', groups='base.group_system')
adyen_api_key = fields.Char( adyen_api_key = fields.Char(
string="API Key", help="The API key of the webservice user", required_if_provider='adyen', string="API Key", help="The API key of the webservice user", required_if_provider='adyen',
@@ -58,7 +58,7 @@ class PaymentAcquirer(models.Model):
:return: None :return: None
""" """
for field_name in ('adyen_checkout_api_url', 'adyen_recurring_api_url'): for field_name in ('adyen_checkout_api_url', 'adyen_recurring_api_url'):
if values.get(field_name): # Test the value in case we're duplicating an acquirer if values.get(field_name): # Test the value in case we're duplicating a provider
values[field_name] = re.sub(r'[vV]\d+(/.*)?', '', values[field_name]) values[field_name] = re.sub(r'[vV]\d+(/.*)?', '', values[field_name])
#=== COMPUTE METHODS ===# #=== COMPUTE METHODS ===#
@@ -66,7 +66,7 @@ class PaymentAcquirer(models.Model):
def _compute_feature_support_fields(self): def _compute_feature_support_fields(self):
""" Override of `payment` to enable additional features. """ """ Override of `payment` to enable additional features. """
super()._compute_feature_support_fields() super()._compute_feature_support_fields()
self.filtered(lambda acq: acq.provider == 'adyen').update({ self.filtered(lambda p: p.code == 'adyen').update({
'support_manual_capture': True, 'support_manual_capture': True,
'support_refund': 'partial', 'support_refund': 'partial',
'support_tokenization': True, 'support_tokenization': True,
@@ -84,7 +84,7 @@ class PaymentAcquirer(models.Model):
:param str url_field_name: The name of the field holding the base URL for the request :param str url_field_name: The name of the field holding the base URL for the request
:param str endpoint: The endpoint to be reached by the request :param str endpoint: The endpoint to be reached by the request
:param str endpoint_param: A variable required by some endpoints which are interpolated with :param str endpoint_param: A variable required by some endpoints which are interpolated with
it if provided. For example, the acquirer reference of the source it if provided. For example, the provider reference of the source
transaction for the '/payments/{}/refunds' endpoint. transaction for the '/payments/{}/refunds' endpoint.
:param dict payload: The payload of the request :param dict payload: The payload of the request
:param str method: The HTTP method of the request :param str method: The HTTP method of the request
@@ -23,11 +23,11 @@ class PaymentTransaction(models.Model):
Note: self.ensure_one() from `_get_processing_values` Note: self.ensure_one() from `_get_processing_values`
:param dict processing_values: The generic processing values of the transaction :param dict processing_values: The generic processing values of the transaction
:return: The dict of acquirer-specific processing values :return: The dict of provider-specific processing values
:rtype: dict :rtype: dict
""" """
res = super()._get_specific_processing_values(processing_values) res = super()._get_specific_processing_values(processing_values)
if self.provider != 'adyen': if self.provider_code != 'adyen':
return res return res
converted_amount = payment_utils.to_minor_currency_units( converted_amount = payment_utils.to_minor_currency_units(
@@ -51,7 +51,7 @@ class PaymentTransaction(models.Model):
:raise: UserError if the transaction is not linked to a token :raise: UserError if the transaction is not linked to a token
""" """
super()._send_payment_request() super()._send_payment_request()
if self.provider != 'adyen': if self.provider_code != 'adyen':
return return
# Prepare the payment request to Adyen # Prepare the payment request to Adyen
@@ -62,14 +62,14 @@ class PaymentTransaction(models.Model):
self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name) self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name)
) )
data = { data = {
'merchantAccount': self.acquirer_id.adyen_merchant_account, 'merchantAccount': self.provider_id.adyen_merchant_account,
'amount': { 'amount': {
'value': converted_amount, 'value': converted_amount,
'currency': self.currency_id.name, 'currency': self.currency_id.name,
}, },
'reference': self.reference, 'reference': self.reference,
'paymentMethod': { 'paymentMethod': {
'recurringDetailReference': self.token_id.acquirer_ref, 'recurringDetailReference': self.token_id.provider_ref,
}, },
'shopperReference': self.token_id.adyen_shopper_reference, 'shopperReference': self.token_id.adyen_shopper_reference,
'recurringProcessingModel': 'Subscription', 'recurringProcessingModel': 'Subscription',
@@ -77,18 +77,18 @@ class PaymentTransaction(models.Model):
'shopperInteraction': 'ContAuth', 'shopperInteraction': 'ContAuth',
} }
# Force the capture delay on Adyen side if the acquirer is not configured for capturing # Force the capture delay on Adyen side if the provider is not configured for capturing
# payments manually. This is necessary because it's not possible to distinguish # payments manually. This is necessary because it's not possible to distinguish
# 'AUTHORISATION' events sent by Adyen with the merchant account's capture delay set to # 'AUTHORISATION' events sent by Adyen with the merchant account's capture delay set to
# 'manual' from events with the capture delay set to 'immediate' or a number of hours. If # 'manual' from events with the capture delay set to 'immediate' or a number of hours. If
# the merchant account is configured to capture payments with a delay but the acquirer is # the merchant account is configured to capture payments with a delay but the provider is
# not, we force the immediate capture to avoid considering authorized transactions as # not, we force the immediate capture to avoid considering authorized transactions as
# captured on Odoo. # captured on Odoo.
if not self.acquirer_id.capture_manually: if not self.provider_id.capture_manually:
data.update(captureDelayHours=0) data.update(captureDelayHours=0)
# Make the payment request to Adyen # Make the payment request to Adyen
response_content = self.acquirer_id._adyen_make_request( response_content = self.provider_id._adyen_make_request(
url_field_name='adyen_checkout_api_url', url_field_name='adyen_checkout_api_url',
endpoint='/payments', endpoint='/payments',
payload=data, payload=data,
@@ -112,7 +112,7 @@ class PaymentTransaction(models.Model):
:return: The refund transaction if any :return: The refund transaction if any
:rtype: recordset of `payment.transaction` :rtype: recordset of `payment.transaction`
""" """
if self.provider != 'adyen': if self.provider_code != 'adyen':
return super()._send_refund_request( return super()._send_refund_request(
amount_to_refund=amount_to_refund, amount_to_refund=amount_to_refund,
create_refund_transaction=create_refund_transaction create_refund_transaction=create_refund_transaction
@@ -128,17 +128,17 @@ class PaymentTransaction(models.Model):
arbitrary_decimal_number=CURRENCY_DECIMALS.get(refund_tx.currency_id.name) arbitrary_decimal_number=CURRENCY_DECIMALS.get(refund_tx.currency_id.name)
) )
data = { data = {
'merchantAccount': self.acquirer_id.adyen_merchant_account, 'merchantAccount': self.provider_id.adyen_merchant_account,
'amount': { 'amount': {
'value': converted_amount, 'value': converted_amount,
'currency': refund_tx.currency_id.name, 'currency': refund_tx.currency_id.name,
}, },
'reference': refund_tx.reference, 'reference': refund_tx.reference,
} }
response_content = refund_tx.acquirer_id._adyen_make_request( response_content = refund_tx.provider_id._adyen_make_request(
url_field_name='adyen_checkout_api_url', url_field_name='adyen_checkout_api_url',
endpoint='/payments/{}/refunds', endpoint='/payments/{}/refunds',
endpoint_param=self.acquirer_reference, endpoint_param=self.provider_reference,
payload=data, payload=data,
method='POST' method='POST'
) )
@@ -153,7 +153,7 @@ class PaymentTransaction(models.Model):
if psp_reference and status == 'received': if psp_reference and status == 'received':
# The PSP reference associated with this /refunds request is different from the psp # The PSP reference associated with this /refunds request is different from the psp
# reference associated with the original payment request. # reference associated with the original payment request.
refund_tx.acquirer_reference = psp_reference refund_tx.provider_reference = psp_reference
return refund_tx return refund_tx
@@ -165,24 +165,24 @@ class PaymentTransaction(models.Model):
:return: None :return: None
""" """
super()._send_capture_request() super()._send_capture_request()
if self.provider != 'adyen': if self.provider_code != 'adyen':
return return
converted_amount = payment_utils.to_minor_currency_units( converted_amount = payment_utils.to_minor_currency_units(
self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name) self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name)
) )
data = { data = {
'merchantAccount': self.acquirer_id.adyen_merchant_account, 'merchantAccount': self.provider_id.adyen_merchant_account,
'amount': { 'amount': {
'value': converted_amount, 'value': converted_amount,
'currency': self.currency_id.name, 'currency': self.currency_id.name,
}, },
'reference': self.reference, 'reference': self.reference,
} }
response_content = self.acquirer_id._adyen_make_request( response_content = self.provider_id._adyen_make_request(
url_field_name='adyen_checkout_api_url', url_field_name='adyen_checkout_api_url',
endpoint='/payments/{}/captures', endpoint='/payments/{}/captures',
endpoint_param=self.acquirer_reference, endpoint_param=self.provider_reference,
payload=data, payload=data,
method='POST', method='POST',
) )
@@ -193,7 +193,7 @@ class PaymentTransaction(models.Model):
if status == 'received': if status == 'received':
self._log_message_on_linked_documents(_( self._log_message_on_linked_documents(_(
"The capture of the transaction with reference %s has been requested (%s).", "The capture of the transaction with reference %s has been requested (%s).",
self.reference, self.acquirer_id.name self.reference, self.provider_id.name
)) ))
def _send_void_request(self): def _send_void_request(self):
@@ -204,17 +204,17 @@ class PaymentTransaction(models.Model):
:return: None :return: None
""" """
super()._send_void_request() super()._send_void_request()
if self.provider != 'adyen': if self.provider_code != 'adyen':
return return
data = { data = {
'merchantAccount': self.acquirer_id.adyen_merchant_account, 'merchantAccount': self.provider_id.adyen_merchant_account,
'reference': self.reference, 'reference': self.reference,
} }
response_content = self.acquirer_id._adyen_make_request( response_content = self.provider_id._adyen_make_request(
url_field_name='adyen_checkout_api_url', url_field_name='adyen_checkout_api_url',
endpoint='/payments/{}/cancels', endpoint='/payments/{}/cancels',
endpoint_param=self.acquirer_reference, endpoint_param=self.provider_reference,
payload=data, payload=data,
method='POST', method='POST',
) )
@@ -225,21 +225,21 @@ class PaymentTransaction(models.Model):
if status == 'received': if status == 'received':
self._log_message_on_linked_documents(_( self._log_message_on_linked_documents(_(
"A request was sent to void the transaction with reference %s (%s).", "A request was sent to void the transaction with reference %s (%s).",
self.reference, self.acquirer_id.name self.reference, self.provider_id.name
)) ))
def _get_tx_from_notification_data(self, provider, notification_data): def _get_tx_from_notification_data(self, provider_code, notification_data):
""" Override of payment to find the transaction based on Adyen data. """ Override of payment to find the transaction based on Adyen data.
:param str provider: The provider of the acquirer that handled the transaction :param str provider_code: The code of the provider that handled the transaction
:param dict notification_data: The notification data sent by the provider :param dict notification_data: The notification data sent by the provider
:return: The transaction if found :return: The transaction if found
:rtype: recordset of `payment.transaction` :rtype: recordset of `payment.transaction`
:raise: ValidationError if inconsistent data were received :raise: ValidationError if inconsistent data were received
:raise: ValidationError if the data match no transaction :raise: ValidationError if the data match no transaction
""" """
tx = super()._get_tx_from_notification_data(provider, notification_data) tx = super()._get_tx_from_notification_data(provider_code, notification_data)
if provider != 'adyen' or len(tx) == 1: if provider_code != 'adyen' or len(tx) == 1:
return tx return tx
reference = notification_data.get('merchantReference') reference = notification_data.get('merchantReference')
@@ -247,28 +247,28 @@ class PaymentTransaction(models.Model):
raise ValidationError("Adyen: " + _("Received data with missing merchant reference")) raise ValidationError("Adyen: " + _("Received data with missing merchant reference"))
event_code = notification_data.get('eventCode', 'AUTHORISATION') # Fallback on auth if S2S. event_code = notification_data.get('eventCode', 'AUTHORISATION') # Fallback on auth if S2S.
acquirer_reference = notification_data.get('pspReference') provider_reference = notification_data.get('pspReference')
source_reference = notification_data.get('originalReference') source_reference = notification_data.get('originalReference')
if event_code == 'AUTHORISATION': if event_code == 'AUTHORISATION':
tx = self.search([('reference', '=', reference), ('provider', '=', 'adyen')]) tx = self.search([('reference', '=', reference), ('provider_code', '=', 'adyen')])
elif event_code in ['CAPTURE', 'CANCELLATION']: elif event_code in ['CAPTURE', 'CANCELLATION']:
# The capture/void may be initiated from Adyen, so we can't trust the reference. # The capture/void may be initiated from Adyen, so we can't trust the reference.
# We find the transaction based on the original acquirer reference since Adyen will have # We find the transaction based on the original provider reference since Adyen will have
# two different references: one for the original transaction and one for the capture. # two different references: one for the original transaction and one for the capture.
tx = self.search( tx = self.search(
[('acquirer_reference', '=', source_reference), ('provider', '=', 'adyen')] [('provider_reference', '=', source_reference), ('provider_code', '=', 'adyen')]
) )
else: # 'REFUND' else: # 'REFUND'
# The refund may be initiated from Adyen, so we can't trust the reference, which could # The refund may be initiated from Adyen, so we can't trust the reference, which could
# be identical to another existing transaction. We find the transaction based on the # be identical to another existing transaction. We find the transaction based on the
# acquirer reference. # provider reference.
tx = self.search( tx = self.search(
[('acquirer_reference', '=', acquirer_reference), ('provider', '=', 'adyen')] [('provider_reference', '=', provider_reference), ('provider_code', '=', 'adyen')]
) )
if not tx: # The refund was initiated from Adyen if not tx: # The refund was initiated from Adyen
# Find the source transaction based on the original reference # Find the source transaction based on the original reference
source_tx = self.search( source_tx = self.search(
[('acquirer_reference', '=', source_reference), ('provider', '=', 'adyen')] [('provider_reference', '=', source_reference), ('provider_code', '=', 'adyen')]
) )
if source_tx: if source_tx:
# Manually create a refund transaction with a new reference. The reference of # Manually create a refund transaction with a new reference. The reference of
@@ -296,9 +296,9 @@ class PaymentTransaction(models.Model):
:rtype: recordset of `payment.transaction` :rtype: recordset of `payment.transaction`
:raise: ValidationError if inconsistent data were received :raise: ValidationError if inconsistent data were received
""" """
refund_acquirer_reference = notification_data.get('pspReference') refund_provider_reference = notification_data.get('pspReference')
amount_to_refund = notification_data.get('amount', {}).get('value') amount_to_refund = notification_data.get('amount', {}).get('value')
if not refund_acquirer_reference or not amount_to_refund: if not refund_provider_reference or not amount_to_refund:
raise ValidationError( raise ValidationError(
"Adyen: " + _("Received refund data with missing transaction values") "Adyen: " + _("Received refund data with missing transaction values")
) )
@@ -307,7 +307,7 @@ class PaymentTransaction(models.Model):
amount_to_refund, source_tx.currency_id amount_to_refund, source_tx.currency_id
) )
return source_tx._create_refund_transaction( return source_tx._create_refund_transaction(
amount_to_refund=converted_amount, acquirer_reference=refund_acquirer_reference amount_to_refund=converted_amount, provider_reference=refund_provider_reference
) )
def _process_notification_data(self, notification_data): def _process_notification_data(self, notification_data):
@@ -320,7 +320,7 @@ class PaymentTransaction(models.Model):
:raise: ValidationError if inconsistent data were received :raise: ValidationError if inconsistent data were received
""" """
super()._process_notification_data(notification_data) super()._process_notification_data(notification_data)
if self.provider != 'adyen': if self.provider_code != 'adyen':
return return
# Extract or assume the event code. If none is provided, the feedback data originate from a # Extract or assume the event code. If none is provided, the feedback data originate from a
@@ -328,10 +328,10 @@ class PaymentTransaction(models.Model):
# webhook notification. # webhook notification.
event_code = notification_data.get('eventCode', 'AUTHORISATION') event_code = notification_data.get('eventCode', 'AUTHORISATION')
# Handle the acquirer reference. If the event code is 'CAPTURE' or 'CANCELLATION', we # Handle the provider reference. If the event code is 'CAPTURE' or 'CANCELLATION', we
# discard the pspReference as it is different from the original pspReference of the tx. # discard the pspReference as it is different from the original pspReference of the tx.
if 'pspReference' in notification_data and event_code in ['AUTHORISATION', 'REFUND']: if 'pspReference' in notification_data and event_code in ['AUTHORISATION', 'REFUND']:
self.acquirer_reference = notification_data.get('pspReference') self.provider_reference = notification_data.get('pspReference')
# Handle the payment state # Handle the payment state
payment_state = notification_data.get('resultCode') payment_state = notification_data.get('resultCode')
@@ -347,7 +347,7 @@ class PaymentTransaction(models.Model):
if self.tokenize and has_token_data: if self.tokenize and has_token_data:
self._adyen_tokenize_from_notification_data(notification_data) self._adyen_tokenize_from_notification_data(notification_data)
if not self.acquirer_id.capture_manually: if not self.provider_id.capture_manually:
self._set_done() self._set_done()
else: # The payment was configured for manual capture. else: # The payment was configured for manual capture.
# Differentiate the state based on the event code. # Differentiate the state based on the event code.
@@ -414,10 +414,10 @@ class PaymentTransaction(models.Model):
additional_data = notification_data['additionalData'] additional_data = notification_data['additionalData']
token = self.env['payment.token'].create({ token = self.env['payment.token'].create({
'acquirer_id': self.acquirer_id.id, 'provider_id': self.provider_id.id,
'payment_details': additional_data.get('cardSummary'), 'payment_details': additional_data.get('cardSummary'),
'partner_id': self.partner_id.id, 'partner_id': self.partner_id.id,
'acquirer_ref': additional_data['recurring.recurringDetailReference'], 'provider_ref': additional_data['recurring.recurringDetailReference'],
'adyen_shopper_reference': additional_data['recurring.shopperReference'], 'adyen_shopper_reference': additional_data['recurring.shopperReference'],
'verified': True, # The payment is authorized, so the payment method is valid 'verified': True, # The payment is authorized, so the payment method is valid
}) })
@@ -26,7 +26,7 @@ odoo.define('payment_adyen.payment_form', require => {
return this._rpc({ return this._rpc({
route: '/payment/adyen/payment_details', route: '/payment/adyen/payment_details',
params: { params: {
'acquirer_id': dropin.acquirerId, 'provider_id': dropin.providerId,
'reference': this.adyenDropin.reference, 'reference': this.adyenDropin.reference,
'payment_details': state.data, 'payment_details': state.data,
}, },
@@ -72,14 +72,14 @@ odoo.define('payment_adyen.payment_form', require => {
// Create the transaction and retrieve the processing values // Create the transaction and retrieve the processing values
return this._rpc({ return this._rpc({
route: this.txContext.transactionRoute, route: this.txContext.transactionRoute,
params: this._prepareTransactionRouteParams('adyen', dropin.acquirerId, 'direct'), params: this._prepareTransactionRouteParams('adyen', dropin.providerId, 'direct'),
}).then(processingValues => { }).then(processingValues => {
this.adyenDropin.reference = processingValues.reference; // Store final reference this.adyenDropin.reference = processingValues.reference; // Store final reference
// Initiate the payment // Initiate the payment
return this._rpc({ return this._rpc({
route: '/payment/adyen/payments', route: '/payment/adyen/payments',
params: { params: {
'acquirer_id': dropin.acquirerId, 'provider_id': dropin.providerId,
'reference': processingValues.reference, 'reference': processingValues.reference,
'converted_amount': processingValues.converted_amount, 'converted_amount': processingValues.converted_amount,
'currency_id': processingValues.currency_id, 'currency_id': processingValues.currency_id,
@@ -112,39 +112,39 @@ odoo.define('payment_adyen.payment_form', require => {
* *
* @override method from payment.payment_form_mixin * @override method from payment.payment_form_mixin
* @private * @private
* @param {string} provider - The provider of the selected payment option's acquirer * @param {string} code - The code of the selected payment option's provider
* @param {number} paymentOptionId - The id of the selected payment option * @param {number} paymentOptionId - The id of the selected payment option
* @param {string} flow - The online payment flow of the selected payment option * @param {string} flow - The online payment flow of the selected payment option
* @return {Promise} * @return {Promise}
*/ */
_prepareInlineForm: function (provider, paymentOptionId, flow) { _prepareInlineForm: function (code, paymentOptionId, flow) {
if (provider !== 'adyen') { if (code !== 'adyen') {
return this._super(...arguments); return this._super(...arguments);
} }
// Check if instantiation of the drop-in is needed // Check if instantiation of the drop-in is needed
if (flow === 'token') { if (flow === 'token') {
return Promise.resolve(); // No drop-in for tokens return Promise.resolve(); // No drop-in for tokens
} else if (this.adyenDropin && this.adyenDropin.acquirerId === paymentOptionId) { } else if (this.adyenDropin && this.adyenDropin.providerId === paymentOptionId) {
this._setPaymentFlow('direct'); // Overwrite the flow even if no re-instantiation this._setPaymentFlow('direct'); // Overwrite the flow even if no re-instantiation
return Promise.resolve(); // Don't re-instantiate if already done for this acquirer return Promise.resolve(); // Don't re-instantiate if already done for this provider
} }
// Overwrite the flow of the select payment option // Overwrite the flow of the select payment option
this._setPaymentFlow('direct'); this._setPaymentFlow('direct');
// Get public information on the acquirer (state, client_key) // Get public information on the provider (state, client_key)
return this._rpc({ return this._rpc({
route: '/payment/adyen/acquirer_info', route: '/payment/adyen/provider_info',
params: { params: {
'acquirer_id': paymentOptionId, 'provider_id': paymentOptionId,
}, },
}).then(acquirerInfo => { }).then(providerInfo => {
// Get the available payment methods // Get the available payment methods
return this._rpc({ return this._rpc({
route: '/payment/adyen/payment_methods', route: '/payment/adyen/payment_methods',
params: { params: {
'acquirer_id': paymentOptionId, 'provider_id': paymentOptionId,
'partner_id': parseInt(this.txContext.partnerId), 'partner_id': parseInt(this.txContext.partnerId),
'amount': this.txContext.amount 'amount': this.txContext.amount
? parseFloat(this.txContext.amount) ? parseFloat(this.txContext.amount)
@@ -157,9 +157,9 @@ odoo.define('payment_adyen.payment_form', require => {
// Instantiate the drop-in // Instantiate the drop-in
const configuration = { const configuration = {
paymentMethodsResponse: paymentMethodsResult, paymentMethodsResponse: paymentMethodsResult,
clientKey: acquirerInfo.client_key, clientKey: providerInfo.client_key,
locale: (this._getContext().lang || 'en-US').replace('_', '-'), locale: (this._getContext().lang || 'en-US').replace('_', '-'),
environment: acquirerInfo.state === 'enabled' ? 'live' : 'test', environment: providerInfo.state === 'enabled' ? 'live' : 'test',
onAdditionalDetails: this._dropinOnAdditionalDetails.bind(this), onAdditionalDetails: this._dropinOnAdditionalDetails.bind(this),
onError: this._dropinOnError.bind(this), onError: this._dropinOnError.bind(this),
onSubmit: this._dropinOnSubmit.bind(this), onSubmit: this._dropinOnSubmit.bind(this),
@@ -175,7 +175,7 @@ odoo.define('payment_adyen.payment_form', require => {
setStatusAutomatically: true, setStatusAutomatically: true,
}, },
).mount(`#o_adyen_dropin_container_${paymentOptionId}`); ).mount(`#o_adyen_dropin_container_${paymentOptionId}`);
this.adyenDropin.acquirerId = paymentOptionId; this.adyenDropin.providerId = paymentOptionId;
}); });
}).guardedCatch((error) => { }).guardedCatch((error) => {
error.event.preventDefault(); error.event.preventDefault();
@@ -192,7 +192,7 @@ odoo.define('payment_adyen.payment_form', require => {
* *
* @override method from payment.payment_form_mixin * @override method from payment.payment_form_mixin
* @private * @private
* @param {string} provider - The provider of the payment option's acquirer * @param {string} provider - The provider of the payment option's provider
* @param {number} paymentOptionId - The id of the payment option handling the transaction * @param {number} paymentOptionId - The id of the payment option handling the transaction
* @param {string} flow - The online payment flow of the transaction * @param {string} flow - The online payment flow of the transaction
* @return {Promise} * @return {Promise}
+6 -6
View File
@@ -9,7 +9,7 @@ class AdyenCommon(PaymentCommon):
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.adyen = cls._prepare_acquirer('adyen', update_values={ cls.adyen = cls._prepare_provider('adyen', update_values={
'adyen_merchant_account': 'dummy', 'adyen_merchant_account': 'dummy',
'adyen_api_key': 'dummy', 'adyen_api_key': 'dummy',
'adyen_client_key': 'dummy', 'adyen_client_key': 'dummy',
@@ -19,7 +19,7 @@ class AdyenCommon(PaymentCommon):
}) })
# Override default values # Override default values
cls.acquirer = cls.adyen cls.provider = cls.adyen
cls.psp_reference = '0123456789ABCDEF' cls.psp_reference = '0123456789ABCDEF'
cls.original_reference = 'FEDCBA9876543210' cls.original_reference = 'FEDCBA9876543210'
@@ -46,7 +46,7 @@ class AdyenCommon(PaymentCommon):
] ]
} }
def _create_transaction(self, *args, acquirer_reference=None, **kwargs): def _create_transaction(self, *args, provider_reference=None, **kwargs):
if not acquirer_reference: if not provider_reference:
acquirer_reference = self.psp_reference provider_reference = self.psp_reference
return super()._create_transaction(*args, acquirer_reference=acquirer_reference, **kwargs) return super()._create_transaction(*args, provider_reference=provider_reference, **kwargs)
+24 -24
View File
@@ -41,15 +41,15 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
@mute_logger('odoo.addons.payment_adyen.models.payment_transaction') @mute_logger('odoo.addons.payment_adyen.models.payment_transaction')
def test_send_refund_request(self): def test_send_refund_request(self):
self.acquirer.support_refund = 'full_only' # Should simply not be False self.provider.support_refund = 'full_only' # Should simply not be False
tx = self._create_transaction( tx = self._create_transaction(
'redirect', state='done', acquirer_reference='source_reference' 'redirect', state='done', provider_reference='source_reference'
) )
tx._reconcile_after_done() # Create the payment tx._reconcile_after_done() # Create the payment
# Send the refund request # Send the refund request
with patch( with patch(
'odoo.addons.payment_adyen.models.payment_acquirer.PaymentAcquirer._adyen_make_request', 'odoo.addons.payment_adyen.models.payment_provider.PaymentProvider._adyen_make_request',
new=lambda *args, **kwargs: {'pspReference': "refund_reference", 'status': "received"} new=lambda *args, **kwargs: {'pspReference': "refund_reference", 'status': "received"}
): ):
tx._send_refund_request() tx._send_refund_request()
@@ -65,20 +65,20 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
"until a success notification is sent" "until a success notification is sent"
) )
self.assertNotEqual( self.assertNotEqual(
refund_tx.acquirer_reference, refund_tx.provider_reference,
tx.acquirer_reference, tx.provider_reference,
msg="The acquirer reference of the refund transaction should be different from that of " msg="The provider reference of the refund transaction should be different from that of "
"the source transaction." "the source transaction."
) )
def test_get_tx_from_notification_data_returns_refund_tx(self): def test_get_tx_from_notification_data_returns_refund_tx(self):
source_tx = self._create_transaction( source_tx = self._create_transaction(
'direct', state='done', acquirer_reference=self.original_reference 'direct', state='done', provider_reference=self.original_reference
) )
refund_tx = self._create_transaction( refund_tx = self._create_transaction(
'direct', 'direct',
reference='RefundTx', reference='RefundTx',
acquirer_reference=self.psp_reference, provider_reference=self.psp_reference,
amount=-source_tx.amount, amount=-source_tx.amount,
operation='refund', operation='refund',
source_transaction_id=source_tx.id source_transaction_id=source_tx.id
@@ -98,7 +98,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
def test_get_tx_from_notification_data_creates_refund_tx_when_missing(self): def test_get_tx_from_notification_data_creates_refund_tx_when_missing(self):
source_tx = self._create_transaction( source_tx = self._create_transaction(
'direct', state='done', acquirer_reference=self.original_reference 'direct', state='done', provider_reference=self.original_reference
) )
data = dict( data = dict(
self.webhook_notification_payload, self.webhook_notification_payload,
@@ -117,11 +117,11 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
@mute_logger('odoo.addons.payment_adyen.models.payment_transaction') @mute_logger('odoo.addons.payment_adyen.models.payment_transaction')
def test_tx_state_after_send_capture_request(self): def test_tx_state_after_send_capture_request(self):
self.acquirer.capture_manually = True self.provider.capture_manually = True
tx = self._create_transaction('direct', state='authorized') tx = self._create_transaction('direct', state='authorized')
with patch( with patch(
'odoo.addons.payment_adyen.models.payment_acquirer.PaymentAcquirer._adyen_make_request', 'odoo.addons.payment_adyen.models.payment_provider.PaymentProvider._adyen_make_request',
return_value={'status': 'received'}, return_value={'status': 'received'},
): ):
tx._send_capture_request() tx._send_capture_request()
@@ -134,11 +134,11 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
@mute_logger('odoo.addons.payment_adyen.models.payment_transaction') @mute_logger('odoo.addons.payment_adyen.models.payment_transaction')
def test_tx_state_after_send_void_request(self): def test_tx_state_after_send_void_request(self):
self.acquirer.capture_manually = True self.provider.capture_manually = True
tx = self._create_transaction('direct', state='authorized') tx = self._create_transaction('direct', state='authorized')
with patch( with patch(
'odoo.addons.payment_adyen.models.payment_acquirer.PaymentAcquirer._adyen_make_request', 'odoo.addons.payment_adyen.models.payment_provider.PaymentProvider._adyen_make_request',
return_value={'status': 'received'}, return_value={'status': 'received'},
): ):
tx._send_void_request() tx._send_void_request()
@@ -155,7 +155,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
self.assertEqual(tx.state, 'done') self.assertEqual(tx.state, 'done')
def test_webhook_notification_authorizes_transaction(self): def test_webhook_notification_authorizes_transaction(self):
self.acquirer.capture_manually = True self.provider.capture_manually = True
tx = self._create_transaction('direct') tx = self._create_transaction('direct')
self._webhook_notification_flow(self.webhook_notification_batch_data) self._webhook_notification_flow(self.webhook_notification_batch_data)
self.assertEqual( self.assertEqual(
@@ -166,9 +166,9 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
) )
def test_webhook_notification_captures_transaction(self): def test_webhook_notification_captures_transaction(self):
self.acquirer.capture_manually = True self.provider.capture_manually = True
tx = self._create_transaction( tx = self._create_transaction(
'direct', state='authorized', acquirer_reference=self.original_reference 'direct', state='authorized', provider_reference=self.original_reference
) )
payload = dict(self.webhook_notification_batch_data, notificationItems=[{ payload = dict(self.webhook_notification_batch_data, notificationItems=[{
'NotificationRequestItem': dict(self.webhook_notification_payload, eventCode='CAPTURE') 'NotificationRequestItem': dict(self.webhook_notification_payload, eventCode='CAPTURE')
@@ -181,7 +181,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
def test_webhook_notification_cancels_transaction(self): def test_webhook_notification_cancels_transaction(self):
tx = self._create_transaction( tx = self._create_transaction(
'direct', state='pending', acquirer_reference=self.original_reference 'direct', state='pending', provider_reference=self.original_reference
) )
payload = dict(self.webhook_notification_batch_data, notificationItems=[{ payload = dict(self.webhook_notification_batch_data, notificationItems=[{
'NotificationRequestItem': dict( 'NotificationRequestItem': dict(
@@ -197,7 +197,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
def test_webhook_notification_refunds_transaction(self): def test_webhook_notification_refunds_transaction(self):
source_tx = self._create_transaction( source_tx = self._create_transaction(
'direct', state='done', acquirer_reference=self.original_reference 'direct', state='done', provider_reference=self.original_reference
) )
payload = dict(self.webhook_notification_batch_data, notificationItems=[{ payload = dict(self.webhook_notification_batch_data, notificationItems=[{
'NotificationRequestItem': dict( 'NotificationRequestItem': dict(
@@ -232,7 +232,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
def test_failed_webhook_capture_notification_leaves_transaction_authorized(self): def test_failed_webhook_capture_notification_leaves_transaction_authorized(self):
tx = self._create_transaction( tx = self._create_transaction(
'direct', state='authorized', acquirer_reference=self.original_reference 'direct', state='authorized', provider_reference=self.original_reference
) )
payload = dict(self.webhook_notification_batch_data, notificationItems=[{ payload = dict(self.webhook_notification_batch_data, notificationItems=[{
'NotificationRequestItem': dict( 'NotificationRequestItem': dict(
@@ -260,7 +260,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
def test_failed_webhook_refund_notification_sets_refund_transaction_in_error(self): def test_failed_webhook_refund_notification_sets_refund_transaction_in_error(self):
source_tx = self._create_transaction( source_tx = self._create_transaction(
'direct', state='done', acquirer_reference=self.original_reference 'direct', state='done', provider_reference=self.original_reference
) )
payload = dict(self.webhook_notification_batch_data, notificationItems=[{ payload = dict(self.webhook_notification_batch_data, notificationItems=[{
'NotificationRequestItem': dict( 'NotificationRequestItem': dict(
@@ -336,8 +336,8 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
self.assertRaises(Forbidden, AdyenController._verify_notification_signature, payload, tx) self.assertRaises(Forbidden, AdyenController._verify_notification_signature, payload, tx)
def test_adyen_neutralize(self): def test_adyen_neutralize(self):
self.env['payment.acquirer']._neutralize() self.env['payment.provider']._neutralize()
self.assertEqual(self.acquirer.adyen_merchant_account, False) self.assertEqual(self.provider.adyen_merchant_account, False)
self.assertEqual(self.acquirer.adyen_api_key, False) self.assertEqual(self.provider.adyen_api_key, False)
self.assertEqual(self.acquirer.adyen_hmac_key, False) self.assertEqual(self.provider.adyen_hmac_key, False)
@@ -2,7 +2,7 @@
<odoo> <odoo>
<template id="inline_form"> <template id="inline_form">
<div t-attf-id="o_adyen_dropin_container_{{acquirer_id}}" class="o_adyen_dropin"/> <div t-attf-id="o_adyen_dropin_container_{{provider_id}}" class="o_adyen_dropin"/>
</template> </template>
</odoo> </odoo>
+17 -17
View File
@@ -1,32 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<record id="payment_acquirer_form" model="ir.ui.view"> <record id="payment_provider_form" model="ir.ui.view">
<field name="name">Adyen Acquirer Form</field> <field name="name">Adyen Provider Form</field>
<field name="model">payment.acquirer</field> <field name="model">payment.provider</field>
<field name="inherit_id" ref="payment.payment_acquirer_form"/> <field name="inherit_id" ref="payment.payment_provider_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='inside'> <group name="code" position='inside'>
<group attrs="{'invisible': [('provider', '!=', 'adyen')]}"> <group attrs="{'invisible': [('code', '!=', 'adyen')]}">
<field name="adyen_merchant_account" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/> <field name="adyen_merchant_account" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
<field name="adyen_api_key" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}" password="True"/> <field name="adyen_api_key" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}" password="True"/>
<field name="adyen_client_key" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/> <field name="adyen_client_key" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
<field name="adyen_hmac_key" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}" password="True"/> <field name="adyen_hmac_key" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}" password="True"/>
<field name="adyen_checkout_api_url" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/> <field name="adyen_checkout_api_url" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
<field name="adyen_recurring_api_url" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/> <field name="adyen_recurring_api_url" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
</group> </group>
</xpath> </group>
<xpath expr='//group[@name="acquirer_config"]' position='before'> <group name="provider_config" position='before'>
<div attrs="{'invisible':['|', ('provider', '!=', 'adyen'), ('capture_manually', '=', False)]}" <div attrs="{'invisible':['|', ('code', '!=', 'adyen'), ('capture_manually', '=', False)]}"
class="alert alert-warning" class="alert alert-warning"
role="alert"> role="alert">
<strong>Warning:</strong> To capture the amount manually, you also need to set <strong>Warning:</strong> To capture the amount manually, you also need to set
the Capture Delay to manual on your Adyen account settings. the Capture Delay to manual on your Adyen account settings.
<a href="https://www.odoo.com/documentation/master/applications/finance/payment_acquirers/adyen.html#place-a-hold-on-a-card" <a href="https://www.odoo.com/documentation/master/applications/finance/payment_providers/adyen.html#place-a-hold-on-a-card"
title="Learn More" title="Learn More"
target="_blank">Learn More</a> target="_blank">Learn More</a>
</div> </div>
</xpath> </group>
</field> </field>
</record> </record>
+3 -3
View File
@@ -6,14 +6,14 @@ from . import models
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools import config from odoo.tools import config
from odoo.addons.payment import setup_provider, reset_payment_acquirer from odoo.addons.payment import setup_provider, reset_payment_provider
def pre_init_hook(cr): def pre_init_hook(cr):
if not any(config.get(key) for key in ('init', 'update')): if not any(config.get(key) for key in ('init', 'update')):
raise UserError( raise UserError(
"This module is deprecated and cannot be installed. " "This module is deprecated and cannot be installed. "
"Consider installing the Payment Acquirer: AsiaPay module instead.") "Consider installing the Payment Provider: AsiaPay module instead.")
def post_init_hook(cr, registry): def post_init_hook(cr, registry):
@@ -21,4 +21,4 @@ def post_init_hook(cr, registry):
def uninstall_hook(cr, registry): def uninstall_hook(cr, registry):
reset_payment_acquirer(cr, registry, 'alipay') reset_payment_provider(cr, registry, 'alipay')
+3 -3
View File
@@ -1,8 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
{ {
'name': 'Alipay Payment Acquirer', 'name': 'Alipay Payment Provider',
'category': 'Accounting/Payment Acquirers', 'category': 'Accounting/Payment Providers',
'version': '2.0', 'version': '2.0',
'sequence': 345, 'sequence': 345,
'summary': "This module is deprecated.", 'summary': "This module is deprecated.",
@@ -10,7 +10,7 @@
'data': [ 'data': [
'views/payment_alipay_templates.xml', 'views/payment_alipay_templates.xml',
'views/payment_views.xml', 'views/payment_views.xml',
'data/payment_acquirer_data.xml', 'data/payment_provider_data.xml',
], ],
'application': False, 'application': False,
'pre_init_hook': 'pre_init_hook', 'pre_init_hook': 'pre_init_hook',
+3 -3
View File
@@ -76,10 +76,10 @@ class AlipayController(http.Controller):
:return: None :return: None
:raise: :class:`werkzeug.exceptions.Forbidden` if the notification origin can't be verified :raise: :class:`werkzeug.exceptions.Forbidden` if the notification origin can't be verified
""" """
url = tx_sudo.acquirer_id._alipay_get_api_url() url = tx_sudo.provider_id._alipay_get_api_url()
payload = { payload = {
'service': 'notify_verify', 'service': 'notify_verify',
'partner': tx_sudo.acquirer_id.alipay_merchant_partner_id, 'partner': tx_sudo.provider_id.alipay_merchant_partner_id,
'notify_id': notification_data['notify_id'], 'notify_id': notification_data['notify_id'],
} }
try: try:
@@ -116,7 +116,7 @@ class AlipayController(http.Controller):
raise Forbidden() raise Forbidden()
# Compare the received signature with the expected signature computed from the data # Compare the received signature with the expected signature computed from the data
expected_signature = tx_sudo.acquirer_id._alipay_compute_signature(notification_data) expected_signature = tx_sudo.provider_id._alipay_compute_signature(notification_data)
if not hmac.compare_digest(received_signature, expected_signature): if not hmac.compare_digest(received_signature, expected_signature):
_logger.warning("received notification with invalid signature") _logger.warning("received notification with invalid signature")
raise Forbidden() raise Forbidden()
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1"> <odoo noupdate="1">
<record id="payment_acquirer_alipay" model="payment.acquirer"> <record id="payment_provider_alipay" model="payment.provider">
<field name="name">Alipay</field> <field name="name">Alipay</field>
<field name="display_as">Credit Card (powered by Alipay)</field> <field name="display_as">Credit Card (powered by Alipay)</field>
<field name="image_128" type="base64" file="payment_alipay/static/description/icon.png"/> <field name="image_128" type="base64" file="payment_alipay/static/description/icon.png"/>
@@ -15,7 +15,7 @@
ref('payment.payment_icon_cc_webmoney'), ref('payment.payment_icon_cc_webmoney'),
ref('payment.payment_icon_cc_visa'), ref('payment.payment_icon_cc_visa'),
])]"/> ])]"/>
<field name="provider">alipay</field> <field name="code">alipay</field>
<field name="redirect_form_view_id" ref="redirect_form"/> <field name="redirect_form_view_id" ref="redirect_form"/>
</record> </record>
+1 -1
View File
@@ -1,4 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment_acquirer from . import payment_provider
from . import payment_transaction from . import payment_transaction
@@ -8,10 +8,10 @@ from odoo import api, fields, models
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class PaymentAcquirer(models.Model): class PaymentProvider(models.Model):
_inherit = 'payment.acquirer' _inherit = 'payment.provider'
provider = fields.Selection( code = fields.Selection(
selection_add=[('alipay', "Alipay")], ondelete={'alipay': 'set default'}) selection_add=[('alipay', "Alipay")], ondelete={'alipay': 'set default'})
alipay_payment_method = fields.Selection( alipay_payment_method = fields.Selection(
string="Account", string="Account",
@@ -33,24 +33,24 @@ class PaymentAcquirer(models.Model):
def _compute_feature_support_fields(self): def _compute_feature_support_fields(self):
""" Override of `payment` to enable additional features. """ """ Override of `payment` to enable additional features. """
super()._compute_feature_support_fields() super()._compute_feature_support_fields()
self.filtered(lambda acq: acq.provider == 'alipay').update({ self.filtered(lambda p: p.code == 'alipay').update({
'support_fees': True, 'support_fees': True,
}) })
# === BUSINESS METHODS ===# # === BUSINESS METHODS ===#
@api.model @api.model
def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs): def _get_compatible_providers(self, *args, currency_id=None, **kwargs):
""" Override of payment to unlist Alipay acquirers for unsupported currencies. """ """ Override of payment to unlist Alipay providers for unsupported currencies. """
acquirers = super()._get_compatible_acquirers(*args, currency_id=currency_id, **kwargs) providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs)
currency = self.env['res.currency'].browse(currency_id).exists() currency = self.env['res.currency'].browse(currency_id).exists()
if currency and currency.name != 'CNY': if currency and currency.name != 'CNY':
acquirers = acquirers.filtered( providers = providers.filtered(
lambda a: a.provider != 'alipay' or a.alipay_payment_method != 'express_checkout' lambda p: p.code != 'alipay' or p.alipay_payment_method != 'express_checkout'
) )
return acquirers return providers
def _alipay_compute_signature(self, data): def _alipay_compute_signature(self, data):
# Rearrange parameters in the data set alphabetically # Rearrange parameters in the data set alphabetically
@@ -22,14 +22,14 @@ class PaymentTransaction(models.Model):
Note: self.ensure_one() from `_get_processing_values` Note: self.ensure_one() from `_get_processing_values`
:param dict processing_values: The generic and specific processing values of the transaction :param dict processing_values: The generic and specific processing values of the transaction
:return: The dict of acquirer-specific processing values :return: The dict of provider-specific processing values
:rtype: dict :rtype: dict
""" """
res = super()._get_specific_rendering_values(processing_values) res = super()._get_specific_rendering_values(processing_values)
if self.provider != 'alipay': if self.provider_code != 'alipay':
return res return res
base_url = self.acquirer_id.get_base_url() base_url = self.provider_id.get_base_url()
if self.fees: if self.fees:
# Similarly to what is done in `payment::payment.transaction.create`, we need to round # Similarly to what is done in `payment::payment.transaction.create`, we need to round
# the sum of the amount and of the fees to avoid inconsistent string representations. # the sum of the amount and of the fees to avoid inconsistent string representations.
@@ -41,12 +41,12 @@ class PaymentTransaction(models.Model):
'_input_charset': 'utf-8', '_input_charset': 'utf-8',
'notify_url': urls.url_join(base_url, AlipayController._webhook_url), 'notify_url': urls.url_join(base_url, AlipayController._webhook_url),
'out_trade_no': self.reference, 'out_trade_no': self.reference,
'partner': self.acquirer_id.alipay_merchant_partner_id, 'partner': self.provider_id.alipay_merchant_partner_id,
'return_url': urls.url_join(base_url, AlipayController._return_url), 'return_url': urls.url_join(base_url, AlipayController._return_url),
'subject': self.reference, 'subject': self.reference,
'total_fee': total_fee, 'total_fee': total_fee,
} }
if self.acquirer_id.alipay_payment_method == 'standard_checkout': if self.provider_id.alipay_payment_method == 'standard_checkout':
# https://global.alipay.com/docs/ac/global/create_forex_trade # https://global.alipay.com/docs/ac/global/create_forex_trade
rendering_values.update({ rendering_values.update({
'service': 'create_forex_trade', 'service': 'create_forex_trade',
@@ -57,29 +57,29 @@ class PaymentTransaction(models.Model):
rendering_values.update({ rendering_values.update({
'service': 'create_direct_pay_by_user', 'service': 'create_direct_pay_by_user',
'payment_type': 1, 'payment_type': 1,
'seller_email': self.acquirer_id.alipay_seller_email, 'seller_email': self.provider_id.alipay_seller_email,
}) })
sign = self.acquirer_id._alipay_compute_signature(rendering_values) sign = self.provider_id._alipay_compute_signature(rendering_values)
rendering_values.update({ rendering_values.update({
'sign_type': 'MD5', 'sign_type': 'MD5',
'sign': sign, 'sign': sign,
'api_url': self.acquirer_id._alipay_get_api_url(), 'api_url': self.provider_id._alipay_get_api_url(),
}) })
return rendering_values return rendering_values
def _get_tx_from_notification_data(self, provider, notification_data): def _get_tx_from_notification_data(self, provider_code, notification_data):
""" Override of payment to find the transaction based on Alipay data. """ Override of payment to find the transaction based on Alipay data.
:param str provider: The provider of the acquirer that handled the transaction :param str provider_code: The code of the provider that handled the transaction
:param dict notification_data: The notification data sent by the provider :param dict notification_data: The notification data sent by the provider
:return: The transaction if found :return: The transaction if found
:rtype: recordset of `payment.transaction` :rtype: recordset of `payment.transaction`
:raise: ValidationError if inconsistent data were received :raise: ValidationError if inconsistent data were received
:raise: ValidationError if the data match no transaction :raise: ValidationError if the data match no transaction
""" """
tx = super()._get_tx_from_notification_data(provider, notification_data) tx = super()._get_tx_from_notification_data(provider_code, notification_data)
if provider != 'alipay' or len(tx) == 1: if provider_code != 'alipay' or len(tx) == 1:
return tx return tx
reference = notification_data.get('reference') or notification_data.get('out_trade_no') reference = notification_data.get('reference') or notification_data.get('out_trade_no')
@@ -92,7 +92,7 @@ class PaymentTransaction(models.Model):
) )
) )
tx = self.search([('reference', '=', reference), ('provider', '=', 'alipay')]) tx = self.search([('reference', '=', reference), ('provider_code', '=', 'alipay')])
if not tx: if not tx:
raise ValidationError( raise ValidationError(
"Alipay: " + _("No transaction found matching reference %s.", reference) "Alipay: " + _("No transaction found matching reference %s.", reference)
@@ -110,10 +110,10 @@ class PaymentTransaction(models.Model):
:raise: ValidationError if inconsistent data were received :raise: ValidationError if inconsistent data were received
""" """
super()._process_notification_data(notification_data) super()._process_notification_data(notification_data)
if self.provider != 'alipay': if self.provider_code != 'alipay':
return return
self.acquirer_reference = notification_data.get('trade_no') self.provider_reference = notification_data.get('trade_no')
status = notification_data.get('trade_status') status = notification_data.get('trade_status')
if status in ['TRADE_FINISHED', 'TRADE_SUCCESS']: if status in ['TRADE_FINISHED', 'TRADE_SUCCESS']:
self._set_done() self._set_done()
+2 -2
View File
@@ -10,7 +10,7 @@ class AlipayCommon(PaymentCommon):
super().setUpClass() super().setUpClass()
cls.currency_yuan = cls._prepare_currency('CNY') cls.currency_yuan = cls._prepare_currency('CNY')
cls.alipay = cls._prepare_acquirer('alipay', update_values={ cls.alipay = cls._prepare_provider('alipay', update_values={
'alipay_merchant_partner_id': 'dummy', 'alipay_merchant_partner_id': 'dummy',
'alipay_md5_signature_key': 'dummy', 'alipay_md5_signature_key': 'dummy',
'alipay_seller_email': 'dummy', 'alipay_seller_email': 'dummy',
@@ -18,7 +18,7 @@ class AlipayCommon(PaymentCommon):
}) })
# override defaults for helpers # override defaults for helpers
cls.acquirer = cls.alipay cls.provider = cls.alipay
cls.currency = cls.currency_yuan cls.currency = cls.currency_yuan
cls.notification_data = { cls.notification_data = {
+17 -17
View File
@@ -16,26 +16,26 @@ from odoo.addons.payment_alipay.tests.common import AlipayCommon
@tagged('post_install', '-at_install') @tagged('post_install', '-at_install')
class AlipayTest(AlipayCommon, PaymentHttpCommon): class AlipayTest(AlipayCommon, PaymentHttpCommon):
def test_compatible_acquirers(self): def test_compatible_providers(self):
self.alipay.alipay_payment_method = 'express_checkout' self.alipay.alipay_payment_method = 'express_checkout'
acquirers = self.env['payment.acquirer']._get_compatible_acquirers( providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency_yuan.id self.company.id, self.partner.id, self.amount, currency_id=self.currency_yuan.id
) )
self.assertIn(self.alipay, acquirers) self.assertIn(self.alipay, providers)
acquirers = self.env['payment.acquirer']._get_compatible_acquirers( providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency_euro.id self.company.id, self.partner.id, self.amount, currency_id=self.currency_euro.id
) )
self.assertNotIn(self.alipay, acquirers) self.assertNotIn(self.alipay, providers)
self.alipay.alipay_payment_method = 'standard_checkout' self.alipay.alipay_payment_method = 'standard_checkout'
acquirers = self.env['payment.acquirer']._get_compatible_acquirers( providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency_yuan.id self.company.id, self.partner.id, self.amount, currency_id=self.currency_yuan.id
) )
self.assertIn(self.alipay, acquirers) self.assertIn(self.alipay, providers)
acquirers = self.env['payment.acquirer']._get_compatible_acquirers( providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency_euro.id self.company.id, self.partner.id, self.amount, currency_id=self.currency_euro.id
) )
self.assertIn(self.alipay, acquirers) self.assertIn(self.alipay, providers)
def test_01_redirect_form_standard_checkout(self): def test_01_redirect_form_standard_checkout(self):
self.alipay.alipay_payment_method = 'standard_checkout' self.alipay.alipay_payment_method = 'standard_checkout'
@@ -92,7 +92,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
) )
def test_03_redirect_form_with_fees(self): def test_03_redirect_form_with_fees(self):
# update acquirer: compute fees # update provider: compute fees
self.alipay.write({ self.alipay.write({
'fees_active': True, 'fees_active': True,
'fees_dom_fixed': 1.0, 'fees_dom_fixed': 1.0,
@@ -141,7 +141,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
tx = self._create_transaction('redirect') tx = self._create_transaction('redirect')
self.env['payment.transaction']._handle_notification_data('alipay', self.notification_data) self.env['payment.transaction']._handle_notification_data('alipay', self.notification_data)
self.assertEqual(tx.state, 'done') self.assertEqual(tx.state, 'done')
self.assertEqual(tx.acquirer_reference, self.notification_data['trade_no']) self.assertEqual(tx.provider_reference, self.notification_data['trade_no'])
# Pending transaction # Pending transaction
self.reference = 'Test Transaction 2' self.reference = 'Test Transaction 2'
@@ -155,7 +155,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
@mute_logger('odoo.addons.payment_alipay.controllers.main') @mute_logger('odoo.addons.payment_alipay.controllers.main')
def test_webhook_notification_confirms_transaction(self): def test_webhook_notification_confirms_transaction(self):
""" Test the processing of a webhook notification. """ """ Test the processing of a webhook notification. """
self.acquirer.alipay_payment_method = 'standard_checkout' self.provider.alipay_payment_method = 'standard_checkout'
tx = self._create_transaction('redirect') tx = self._create_transaction('redirect')
url = self._build_url(AlipayController._webhook_url) url = self._build_url(AlipayController._webhook_url)
with patch( with patch(
@@ -171,7 +171,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
@mute_logger('odoo.addons.payment_alipay.controllers.main') @mute_logger('odoo.addons.payment_alipay.controllers.main')
def test_webhook_notification_triggers_origin_and_signature_checks(self): def test_webhook_notification_triggers_origin_and_signature_checks(self):
""" Test that receiving a webhook notification triggers origin and signature checks. """ """ Test that receiving a webhook notification triggers origin and signature checks. """
self.acquirer.alipay_payment_method = 'standard_checkout' self.provider.alipay_payment_method = 'standard_checkout'
self._create_transaction('redirect') self._create_transaction('redirect')
url = self._build_url(AlipayController._webhook_url) url = self._build_url(AlipayController._webhook_url)
with patch( with patch(
@@ -210,8 +210,8 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
self.assertRaises(Forbidden, AlipayController._verify_notification_signature, payload, tx) self.assertRaises(Forbidden, AlipayController._verify_notification_signature, payload, tx)
def test_alipay_neutralize(self): def test_alipay_neutralize(self):
self.env['payment.acquirer']._neutralize() self.env['payment.provider']._neutralize()
self.assertEqual(self.acquirer.alipay_merchant_partner_id, False) self.assertEqual(self.provider.alipay_merchant_partner_id, False)
self.assertEqual(self.acquirer.alipay_md5_signature_key, False) self.assertEqual(self.provider.alipay_md5_signature_key, False)
self.assertEqual(self.acquirer.alipay_seller_email, False) self.assertEqual(self.provider.alipay_seller_email, False)
@@ -1,24 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<record id="payment_acquirer_form" model="ir.ui.view"> <record id="payment_provider_form" model="ir.ui.view">
<field name="name">Alipay Acquirer Form</field> <field name="name">Alipay Provider Form</field>
<field name="model">payment.acquirer</field> <field name="model">payment.provider</field>
<field name="inherit_id" ref="payment.payment_acquirer_form"/> <field name="inherit_id" ref="payment.payment_provider_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//div[@id='provider_creation_warning']" position="after"> <xpath expr="//div[@id='provider_creation_warning']" position="after">
<div class="alert alert-danger" <div class="alert alert-danger"
role="alert" role="alert"
attrs="{'invisible': [('provider', '!=', 'alipay')]}"> attrs="{'invisible': [('code', '!=', 'alipay')]}">
This provider is deprecated. This provider is deprecated.
Consider disabling it and moving to <strong>Asiapay</strong>. Consider disabling it and moving to <strong>Asiapay</strong>.
</div> </div>
</xpath> </xpath>
<group name="acquirer" position="inside"> <group name="code" position="inside">
<group attrs="{'invisible': [('provider', '!=', 'alipay')]}"> <group attrs="{'invisible': [('code', '!=', 'alipay')]}">
<field name="alipay_payment_method" widget="radio"/> <field name="alipay_payment_method" widget="radio"/>
<field name="alipay_seller_email" <field name="alipay_seller_email"
attrs="{'invisible': [('alipay_payment_method', '=', 'standard_checkout')], 'required': [('provider', '=', 'alipay'), ('alipay_payment_method', '=', 'express_checkout')]}"/> attrs="{'invisible': [('alipay_payment_method', '=', 'standard_checkout')], 'required': [('code', '=', 'alipay'), ('alipay_payment_method', '=', 'express_checkout')]}"/>
<field name="alipay_merchant_partner_id"/> <field name="alipay_merchant_partner_id"/>
<field name="alipay_md5_signature_key" password="True"/> <field name="alipay_md5_signature_key" password="True"/>
</group> </group>
+2 -2
View File
@@ -3,7 +3,7 @@
from . import controllers from . import controllers
from . import models from . import models
from odoo.addons.payment import setup_provider, reset_payment_acquirer from odoo.addons.payment import setup_provider, reset_payment_provider
def post_init_hook(cr, registry): def post_init_hook(cr, registry):
@@ -11,4 +11,4 @@ def post_init_hook(cr, registry):
def uninstall_hook(cr, registry): def uninstall_hook(cr, registry):
reset_payment_acquirer(cr, registry, 'aps') reset_payment_provider(cr, registry, 'aps')
+3 -3
View File
@@ -1,9 +1,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
{ {
'name': "Payment Acquirer: Amazon Payment Services", 'name': "Payment Provider: Amazon Payment Services",
'version': '1.0', 'version': '1.0',
'category': 'Accounting/Payment Acquirers', 'category': 'Accounting/Payment Providers',
'sequence': 347, 'sequence': 347,
'summary': "Amazon Payment Services' online payment provider covering the MENA region.", 'summary': "Amazon Payment Services' online payment provider covering the MENA region.",
'depends': ['payment'], 'depends': ['payment'],
@@ -11,7 +11,7 @@
'views/payment_aps_templates.xml', 'views/payment_aps_templates.xml',
'views/payment_views.xml', 'views/payment_views.xml',
'data/payment_acquirer_data.xml', 'data/payment_provider_data.xml',
], ],
'application': True, 'application': True,
'post_init_hook': 'post_init_hook', 'post_init_hook': 'post_init_hook',
+1 -1
View File
@@ -87,7 +87,7 @@ class APSController(http.Controller):
raise Forbidden() raise Forbidden()
# Compare the received signature with the expected signature computed from the data. # Compare the received signature with the expected signature computed from the data.
expected_signature = tx_sudo.acquirer_id._aps_calculate_signature( expected_signature = tx_sudo.provider_id._aps_calculate_signature(
notification_data, incoming=True notification_data, incoming=True
) )
if not hmac.compare_digest(received_signature, expected_signature): if not hmac.compare_digest(received_signature, expected_signature):
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1"> <odoo noupdate="1">
<record id="payment.payment_acquirer_aps" model="payment.acquirer"> <record id="payment.payment_provider_aps" model="payment.provider">
<field name="provider">aps</field> <field name="code">aps</field>
<field name="redirect_form_view_id" ref="redirect_form"/> <field name="redirect_form_view_id" ref="redirect_form"/>
</record> </record>
+1 -1
View File
@@ -1,4 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment_acquirer from . import payment_provider
from . import payment_transaction from . import payment_transaction
@@ -9,15 +9,15 @@ from odoo import fields, models
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class PaymentAcquirer(models.Model): class PaymentProvider(models.Model):
_inherit = 'payment.acquirer' _inherit = 'payment.provider'
provider = fields.Selection( code = fields.Selection(
selection_add=[('aps', "Amazon Payment Services")], ondelete={'aps': 'set default'} selection_add=[('aps', "Amazon Payment Services")], ondelete={'aps': 'set default'}
) )
aps_merchant_identifier = fields.Char( aps_merchant_identifier = fields.Char(
string="APS Merchant Identifier", string="APS Merchant Identifier",
help="The code of the merchant account to use with this acquirer.", help="The code of the merchant account to use with this provider.",
required_if_provider='aps', required_if_provider='aps',
) )
aps_access_code = fields.Char( aps_access_code = fields.Char(
@@ -19,7 +19,7 @@ class PaymentTransaction(models.Model):
_inherit = 'payment.transaction' _inherit = 'payment.transaction'
@api.model @api.model
def _compute_reference(self, provider, prefix=None, separator='-', **kwargs): def _compute_reference(self, provider_code, prefix=None, separator='-', **kwargs):
""" Override of `payment` to ensure that APS' requirements for references are satisfied. """ Override of `payment` to ensure that APS' requirements for references are satisfied.
APS' requirements for transaction are as follows: APS' requirements for transaction are as follows:
@@ -28,16 +28,16 @@ class PaymentTransaction(models.Model):
generated based on document names that may contain non-allowed characters generated based on document names that may contain non-allowed characters
(eg: INV/2020/...). (eg: INV/2020/...).
:param str provider: The provider of the acquirer handling the transaction. :param str provider_code: The code of the provider handling the transaction.
:param str prefix: The custom prefix used to compute the full reference. :param str prefix: The custom prefix used to compute the full reference.
:param str separator: The custom separator used to separate the prefix from the suffix. :param str separator: The custom separator used to separate the prefix from the suffix.
:return: The unique reference for the transaction. :return: The unique reference for the transaction.
:rtype: str :rtype: str
""" """
if provider == 'aps': if provider_code == 'aps':
prefix = payment_utils.singularize_reference_prefix() prefix = payment_utils.singularize_reference_prefix()
return super()._compute_reference(provider, prefix=prefix, separator=separator, **kwargs) return super()._compute_reference(provider_code, prefix=prefix, separator=separator, **kwargs)
def _get_specific_rendering_values(self, processing_values): def _get_specific_rendering_values(self, processing_values):
""" Override of `payment` to return APS-specific processing values. """ Override of `payment` to return APS-specific processing values.
@@ -45,19 +45,19 @@ class PaymentTransaction(models.Model):
Note: self.ensure_one() from `_get_processing_values` Note: self.ensure_one() from `_get_processing_values`
:param dict processing_values: The generic processing values of the transaction. :param dict processing_values: The generic processing values of the transaction.
:return: The dict of acquirer-specific processing values. :return: The dict of provider-specific processing values.
:rtype: dict :rtype: dict
""" """
res = super()._get_specific_rendering_values(processing_values) res = super()._get_specific_rendering_values(processing_values)
if self.provider != 'aps': if self.provider_code != 'aps':
return res return res
converted_amount = payment_utils.to_minor_currency_units(self.amount, self.currency_id) converted_amount = payment_utils.to_minor_currency_units(self.amount, self.currency_id)
base_url = self.acquirer_id.get_base_url() base_url = self.provider_id.get_base_url()
rendering_values = { rendering_values = {
'command': 'PURCHASE', 'command': 'PURCHASE',
'access_code': self.acquirer_id.aps_access_code, 'access_code': self.provider_id.aps_access_code,
'merchant_identifier': self.acquirer_id.aps_merchant_identifier, 'merchant_identifier': self.provider_id.aps_merchant_identifier,
'merchant_reference': self.reference, 'merchant_reference': self.reference,
'amount': str(converted_amount), 'amount': str(converted_amount),
'currency': self.currency_id.name, 'currency': self.currency_id.name,
@@ -66,25 +66,25 @@ class PaymentTransaction(models.Model):
'return_url': urls.url_join(base_url, APSController._return_url), 'return_url': urls.url_join(base_url, APSController._return_url),
} }
rendering_values.update({ rendering_values.update({
'signature': self.acquirer_id._aps_calculate_signature( 'signature': self.provider_id._aps_calculate_signature(
rendering_values, incoming=False rendering_values, incoming=False
), ),
'api_url': self.acquirer_id._aps_get_api_url(), 'api_url': self.provider_id._aps_get_api_url(),
}) })
return rendering_values return rendering_values
def _get_tx_from_notification_data(self, provider, notification_data): def _get_tx_from_notification_data(self, provider_code, notification_data):
""" Override of `payment` to find the transaction based on APS data. """ Override of `payment` to find the transaction based on APS data.
:param str provider: The provider of the acquirer that handled the transaction. :param str provider_code: The code of the provider that handled the transaction.
:param dict notification_data: The notification data sent by the provider. :param dict notification_data: The notification data sent by the provider.
:return: The transaction if found. :return: The transaction if found.
:rtype: recordset of `payment.transaction` :rtype: recordset of `payment.transaction`
:raise ValidationError: If inconsistent data are received. :raise ValidationError: If inconsistent data are received.
:raise ValidationError: If the data match no transaction. :raise ValidationError: If the data match no transaction.
""" """
tx = super()._get_tx_from_notification_data(provider, notification_data) tx = super()._get_tx_from_notification_data(provider_code, notification_data)
if provider != 'aps' or len(tx) == 1: if provider_code != 'aps' or len(tx) == 1:
return tx return tx
reference = notification_data.get('merchant_reference') reference = notification_data.get('merchant_reference')
@@ -93,7 +93,7 @@ class PaymentTransaction(models.Model):
"APS: " + _("Received data with missing reference %(ref)s.", ref=reference) "APS: " + _("Received data with missing reference %(ref)s.", ref=reference)
) )
tx = self.search([('reference', '=', reference), ('provider', '=', 'aps')]) tx = self.search([('reference', '=', reference), ('provider_code', '=', 'aps')])
if not tx: if not tx:
raise ValidationError( raise ValidationError(
"APS: " + _("No transaction found matching reference %s.", reference) "APS: " + _("No transaction found matching reference %s.", reference)
@@ -111,10 +111,10 @@ class PaymentTransaction(models.Model):
:raise ValidationError: If inconsistent data are received. :raise ValidationError: If inconsistent data are received.
""" """
super()._process_notification_data(notification_data) super()._process_notification_data(notification_data)
if self.provider != 'aps': if self.provider_code != 'aps':
return return
self.acquirer_reference = notification_data.get('fort_id') self.provider_reference = notification_data.get('fort_id')
status = notification_data.get('status') status = notification_data.get('status')
if not status: if not status:
+1 -1
View File
@@ -1,6 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common from . import common
from . import test_payment_acquirer from . import test_payment_provider
from . import test_payment_transaction from . import test_payment_transaction
from . import test_processing_flows from . import test_processing_flows
+4 -4
View File
@@ -9,17 +9,17 @@ class APSCommon(PaymentHttpCommon):
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.aps = cls._prepare_acquirer('aps', update_values={ cls.aps = cls._prepare_provider('aps', update_values={
'aps_merchant_identifier': '123456abc', 'aps_merchant_identifier': '123456abc',
'aps_access_code': 'dummy', 'aps_access_code': 'dummy',
'aps_sha_request': 'dummy', 'aps_sha_request': 'dummy',
'aps_sha_response': 'dummy', 'aps_sha_response': 'dummy',
}) })
cls.acquirer = cls.aps cls.provider = cls.aps
cls.notification_data = { cls.notification_data = {
'access_code': cls.acquirer.aps_access_code, 'access_code': cls.provider.aps_access_code,
'amount': cls.amount, 'amount': cls.amount,
'authorization_code': '123456', 'authorization_code': '123456',
'card_holder_name': 'Mitchell', 'card_holder_name': 'Mitchell',
@@ -32,7 +32,7 @@ class APSCommon(PaymentHttpCommon):
'expiry_date': '2212', 'expiry_date': '2212',
'fort_id': '169996210006464984', 'fort_id': '169996210006464984',
'language': 'en', 'language': 'en',
'merchant_identifier': cls.acquirer.aps_merchant_identifier, 'merchant_identifier': cls.provider.aps_merchant_identifier,
'merchant_reference': cls.reference, 'merchant_reference': cls.reference,
'payment_option': 'VISA', 'payment_option': 'VISA',
'response_code': '14000', 'response_code': '14000',
@@ -1,16 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.payment_aps.tests.common import APSCommon
@tagged('post_install', '-at_install')
class TestPaymentAcquirer(APSCommon):
def test_neutralize(self):
self.env['payment.acquirer']._neutralize()
self.assertEqual(self.acquirer.aps_merchant_identifier, False)
self.assertEqual(self.acquirer.aps_access_code, False)
self.assertEqual(self.acquirer.aps_sha_request, False)
self.assertEqual(self.acquirer.aps_sha_response, False)
@@ -0,0 +1,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.payment_aps.tests.common import APSCommon
@tagged('post_install', '-at_install')
class TestPaymentProvider(APSCommon):
def test_neutralize(self):
self.env['payment.provider']._neutralize()
self.assertEqual(self.provider.aps_merchant_identifier, False)
self.assertEqual(self.provider.aps_access_code, False)
self.assertEqual(self.provider.aps_sha_request, False)
self.assertEqual(self.provider.aps_sha_response, False)
@@ -24,8 +24,8 @@ class TestPaymentTransaction(APSCommon):
converted_amount = payment_utils.to_minor_currency_units(self.amount, self.currency) converted_amount = payment_utils.to_minor_currency_units(self.amount, self.currency)
expected_values = { expected_values = {
'command': 'PURCHASE', 'command': 'PURCHASE',
'access_code': self.acquirer.aps_access_code, 'access_code': self.provider.aps_access_code,
'merchant_identifier': self.acquirer.aps_merchant_identifier, 'merchant_identifier': self.provider.aps_merchant_identifier,
'merchant_reference': tx.reference, 'merchant_reference': tx.reference,
'amount': str(converted_amount), 'amount': str(converted_amount),
'currency': self.currency.name, 'currency': self.currency.name,
@@ -33,7 +33,7 @@ class TestPaymentTransaction(APSCommon):
'customer_email': tx.partner_id.email_normalized, 'customer_email': tx.partner_id.email_normalized,
'return_url': self._build_url(APSController._return_url), 'return_url': self._build_url(APSController._return_url),
'signature': '8f4e295359a578f05fdc6c275829128e7b93440e6c7d13179c1e16cc579c6111', 'signature': '8f4e295359a578f05fdc6c275829128e7b93440e6c7d13179c1e16cc579c6111',
'api_url': self.acquirer._aps_get_api_url(), 'api_url': self.provider._aps_get_api_url(),
} }
self.assertDictEqual(tx._get_specific_rendering_values(None), expected_values) self.assertDictEqual(tx._get_specific_rendering_values(None), expected_values)
+10 -10
View File
@@ -1,27 +1,27 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<record id="payment_acquirer_form" model="ir.ui.view"> <record id="payment_provider_form" model="ir.ui.view">
<field name="name">APS Acquirer Form</field> <field name="name">APS Provider Form</field>
<field name="model">payment.acquirer</field> <field name="model">payment.provider</field>
<field name="inherit_id" ref="payment.payment_acquirer_form"/> <field name="inherit_id" ref="payment.payment_provider_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<group name="acquirer" position='inside'> <group name="code" position='inside'>
<group attrs="{'invisible': [('provider', '!=', 'aps')]}"> <group attrs="{'invisible': [('code', '!=', 'aps')]}">
<field name="aps_merchant_identifier" <field name="aps_merchant_identifier"
string="Merchant Identifier" string="Merchant Identifier"
attrs="{'required': [('provider', '=', 'aps'), ('state', '!=', 'disabled')]}"/> attrs="{'required': [('code', '=', 'aps'), ('state', '!=', 'disabled')]}"/>
<field name="aps_access_code" <field name="aps_access_code"
string="Access Code" string="Access Code"
attrs="{'required': [('provider', '=', 'aps'), ('state', '!=', 'disabled')]}" attrs="{'required': [('code', '=', 'aps'), ('state', '!=', 'disabled')]}"
password="True"/> password="True"/>
<field name="aps_sha_request" <field name="aps_sha_request"
string="SHA Request Phrase" string="SHA Request Phrase"
attrs="{'required': [('provider', '=', 'aps'), ('state', '!=', 'disabled')]}" attrs="{'required': [('code', '=', 'aps'), ('state', '!=', 'disabled')]}"
password="True"/> password="True"/>
<field name="aps_sha_response" <field name="aps_sha_response"
string="SHA Response Phrase" string="SHA Response Phrase"
attrs="{'required': [('provider', '=', 'aps'), ('state', '!=', 'disabled')]}" attrs="{'required': [('code', '=', 'aps'), ('state', '!=', 'disabled')]}"
password="True"/> password="True"/>
</group> </group>
</group> </group>

Some files were not shown because too many files have changed in this diff Show More