[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:
@@ -126,7 +126,7 @@ class AccountJournal(models.Model):
|
||||
copy=False,
|
||||
check_company=True,
|
||||
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"
|
||||
"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.
|
||||
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
|
||||
:return: Either the inbound or outbound payment method lines
|
||||
"""
|
||||
|
||||
@@ -47,7 +47,7 @@ class AccountPayment(models.Model):
|
||||
compute='_compute_payment_method_line_id',
|
||||
domain="[('id', 'in', available_payment_method_line_ids)]",
|
||||
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"
|
||||
"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"
|
||||
|
||||
@@ -67,7 +67,7 @@ class AccountPaymentMethod(models.Model):
|
||||
"""
|
||||
Contains details about how to initialize a payment method with the code x.
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -224,7 +224,7 @@ class ResCompany(models.Model):
|
||||
|
||||
# YTI FIXME: Define only one method that returns {'account': [], 'sale': [], ...}
|
||||
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 [
|
||||
'base_onboarding_company_state',
|
||||
'account_onboarding_invoice_layout_state',
|
||||
|
||||
@@ -88,7 +88,7 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
compute='_compute_payment_method_line_id',
|
||||
domain="[('id', 'in', available_payment_method_line_ids)]",
|
||||
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"
|
||||
"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"
|
||||
|
||||
@@ -10,16 +10,16 @@ from odoo import api, SUPERUSER_ID
|
||||
def post_init_hook(cr, registry):
|
||||
""" Create `account.payment.method` records for the installed payment providers. """
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
PaymentAcquirer = env['payment.acquirer']
|
||||
installed_providers = PaymentAcquirer.search([('module_id.state', '=', 'installed')])
|
||||
for provider_code in set(installed_providers.mapped('provider')):
|
||||
PaymentAcquirer._setup_payment_method(provider_code)
|
||||
PaymentProvider = env['payment.provider']
|
||||
installed_providers = PaymentProvider.search([('module_id.state', '=', 'installed')])
|
||||
for code in set(installed_providers.mapped('code')):
|
||||
PaymentProvider._setup_payment_method(code)
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
""" Delete `account.payment.method` records created for the installed payment providers. """
|
||||
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([
|
||||
('code', 'in', installed_providers.mapped('provider')),
|
||||
('payment_type', '=', 'inbound'),
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
'views/account_move_views.xml',
|
||||
'views/account_journal_views.xml',
|
||||
'views/account_payment_views.xml',
|
||||
'views/payment_acquirer_views.xml',
|
||||
'views/payment_provider_views.xml',
|
||||
'views/payment_transaction_views.xml',
|
||||
|
||||
'wizards/account_payment_register_views.xml',
|
||||
|
||||
@@ -22,23 +22,23 @@ class PortalAccount(portal.PortalAccount):
|
||||
invoice_company = invoice.company_id or request.env.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
|
||||
) # 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(
|
||||
[('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
|
||||
fees_by_acquirer = {
|
||||
acq_sudo: acq_sudo._compute_fees(
|
||||
fees_by_provider = {
|
||||
pro_sudo: pro_sudo._compute_fees(
|
||||
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({
|
||||
'acquirers': acquirers_sudo,
|
||||
'providers': providers_sudo,
|
||||
'tokens': tokens,
|
||||
'fees_by_acquirer': fees_by_acquirer,
|
||||
'fees_by_provider': fees_by_provider,
|
||||
'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,
|
||||
'currency': invoice.currency_id,
|
||||
|
||||
@@ -5,5 +5,5 @@ from . import account_move
|
||||
from . import account_payment
|
||||
from . import account_payment_method
|
||||
from . import account_payment_method_line
|
||||
from . import payment_acquirer
|
||||
from . import payment_provider
|
||||
from . import payment_transaction
|
||||
|
||||
@@ -11,50 +11,52 @@ class AccountJournal(models.Model):
|
||||
@api.constrains('inbound_payment_method_line_ids')
|
||||
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:
|
||||
return
|
||||
|
||||
self.env['account.payment.method'].flush_model(['code', 'payment_type'])
|
||||
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('''
|
||||
SELECT acquirer.id
|
||||
FROM payment_acquirer acquirer
|
||||
SELECT provider.id
|
||||
FROM payment_provider provider
|
||||
JOIN account_payment_method apm
|
||||
ON apm.code = acquirer.provider
|
||||
ON apm.code = provider.code
|
||||
LEFT JOIN account_payment_method_line apml
|
||||
ON apm.id = apml.payment_method_id AND apml.journal_id IS NOT NULL
|
||||
WHERE acquirer.state IN ('enabled', 'test')
|
||||
AND acquirer.provider != 'custom'
|
||||
WHERE provider.state IN ('enabled', 'test')
|
||||
AND provider.code != 'custom'
|
||||
AND apm.payment_type = 'inbound'
|
||||
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)})
|
||||
ids = [r[0] for r in self._cr.fetchall()]
|
||||
if ids:
|
||||
acquirers = self.env['payment.acquirer'].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"
|
||||
"Linked acquirer(s): %s", ', '.join(a.display_name for a in acquirers)))
|
||||
providers = self.env['payment.provider'].sudo().browse(ids)
|
||||
raise UserError(
|
||||
_("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):
|
||||
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')
|
||||
def _compute_available_payment_method_ids(self):
|
||||
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()
|
||||
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}
|
||||
|
||||
# On top of the basic filtering, filter to hide unavailable acquirers.
|
||||
# This avoid allowing payment method lines linked to an acquirer that has no record.
|
||||
# On top of the basic filtering, filter to hide unavailable providers.
|
||||
# This avoid allowing payment method lines linked to a provider that has no record.
|
||||
for code, vals in method_information.items():
|
||||
payment_method = pay_method_by_code.get(code + 'inbound')
|
||||
|
||||
@@ -64,9 +66,9 @@ class AccountJournal(models.Model):
|
||||
for journal in self:
|
||||
to_remove = []
|
||||
|
||||
available_providers = installed_acquirers.filtered(
|
||||
lambda a: a.company_id == journal.company_id
|
||||
).mapped('provider')
|
||||
available_providers = installed_providers.filtered(
|
||||
lambda p: p.company_id == journal.company_id
|
||||
).mapped('code')
|
||||
available = payment_method.code in available_providers
|
||||
|
||||
if vals['mode'] == 'unique' and not available:
|
||||
|
||||
@@ -45,13 +45,13 @@ class AccountMove(models.Model):
|
||||
def payment_action_capture(self):
|
||||
""" Capture all transactions linked to this invoice. """
|
||||
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()
|
||||
|
||||
def payment_action_void(self):
|
||||
""" Void all transactions linked to this invoice. """
|
||||
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()
|
||||
|
||||
def action_view_payment_transactions(self):
|
||||
|
||||
@@ -18,7 +18,7 @@ class AccountPayment(models.Model):
|
||||
string="Saved Payment Token", comodel_name='payment.token', domain="""[
|
||||
('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')
|
||||
|
||||
# == Display purpose fields ==
|
||||
@@ -48,7 +48,7 @@ class AccountPayment(models.Model):
|
||||
def _compute_amount_available_for_refund(self):
|
||||
for payment in self:
|
||||
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
|
||||
# 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
|
||||
@@ -71,9 +71,9 @@ class AccountPayment(models.Model):
|
||||
if payment.use_electronic_payment_method:
|
||||
payment.suitable_payment_token_ids = self.env['payment.token'].sudo().search([
|
||||
('company_id', '=', payment.company_id.id),
|
||||
('acquirer_id.capture_manually', '=', False),
|
||||
('provider_id.capture_manually', '=', False),
|
||||
('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:
|
||||
payment.suitable_payment_token_ids = [Command.clear()]
|
||||
@@ -82,8 +82,8 @@ class AccountPayment(models.Model):
|
||||
def _compute_use_electronic_payment_method(self):
|
||||
for payment in self:
|
||||
# Get a list of all electronic payment method codes.
|
||||
# These codes are comprised of 'electronic' and the providers of each payment acquirer.
|
||||
codes = [key for key in dict(self.env['payment.acquirer']._fields['provider']._description_selection(self.env))]
|
||||
# These codes are comprised of 'electronic' and the providers of each payment provider.
|
||||
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
|
||||
|
||||
def _compute_refunds_count(self):
|
||||
@@ -103,7 +103,7 @@ class AccountPayment(models.Model):
|
||||
|
||||
@api.onchange('partner_id', 'payment_method_line_id', 'journal_id')
|
||||
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):
|
||||
self.payment_token_id = False
|
||||
return
|
||||
@@ -117,20 +117,20 @@ class AccountPayment(models.Model):
|
||||
self.payment_token_id = self.env['payment.token'].sudo().search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('partner_id', 'in', related_partner_ids.ids),
|
||||
('acquirer_id.capture_manually', '=', False),
|
||||
('acquirer_id', '=', self.payment_method_line_id.payment_acquirer_id.id),
|
||||
('provider_id.capture_manually', '=', False),
|
||||
('provider_id', '=', self.payment_method_line_id.payment_provider_id.id),
|
||||
], limit=1)
|
||||
|
||||
#=== ACTION METHODS ===#
|
||||
|
||||
def action_post(self):
|
||||
# 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(
|
||||
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
|
||||
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):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'acquirer_id': self.payment_token_id.acquirer_id.id,
|
||||
'provider_id': self.payment_token_id.provider_id.id,
|
||||
'reference': self.ref,
|
||||
'amount': self.amount,
|
||||
'currency_id': self.currency_id.id,
|
||||
|
||||
@@ -10,10 +10,10 @@ class AccountPaymentMethod(models.Model):
|
||||
@api.model
|
||||
def _get_payment_method_information(self):
|
||||
res = super()._get_payment_method_information()
|
||||
for provider, _desc in self.env['payment.acquirer']._fields['provider'].selection:
|
||||
if provider in ('none', 'transfer'):
|
||||
for code, _desc in self.env['payment.provider']._fields['code'].selection:
|
||||
if code in ('none', 'transfer'):
|
||||
continue
|
||||
res[provider] = {
|
||||
res[code] = {
|
||||
'mode': 'unique',
|
||||
'domain': [('type', '=', 'bank')],
|
||||
}
|
||||
|
||||
@@ -8,35 +8,35 @@ from odoo.osv import expression
|
||||
class AccountPaymentMethodLine(models.Model):
|
||||
_inherit = "account.payment.method.line"
|
||||
|
||||
payment_acquirer_id = fields.Many2one(
|
||||
comodel_name='payment.acquirer',
|
||||
compute='_compute_payment_acquirer_id',
|
||||
payment_provider_id = fields.Many2one(
|
||||
comodel_name='payment.provider',
|
||||
compute='_compute_payment_provider_id',
|
||||
store=True
|
||||
)
|
||||
payment_acquirer_state = fields.Selection(
|
||||
related='payment_acquirer_id.state'
|
||||
payment_provider_state = fields.Selection(
|
||||
related='payment_provider_id.state'
|
||||
)
|
||||
|
||||
@api.depends('payment_method_id')
|
||||
def _compute_payment_acquirer_id(self):
|
||||
acquirers = self.env['payment.acquirer'].sudo().search([
|
||||
('provider', 'in', self.mapped('code')),
|
||||
def _compute_payment_provider_id(self):
|
||||
providers = self.env['payment.provider'].sudo().search([
|
||||
('code', 'in', self.mapped('code')),
|
||||
('company_id', 'in', self.journal_id.company_id.ids),
|
||||
])
|
||||
|
||||
# Make sure to pick the active acquirer, if any.
|
||||
acquirers_map = dict()
|
||||
for acquirer in acquirers:
|
||||
current_value = acquirers_map.get((acquirer.provider, acquirer.company_id), False)
|
||||
# Make sure to pick the active provider, if any.
|
||||
providers_map = dict()
|
||||
for provider in providers:
|
||||
current_value = providers_map.get((provider.code, provider.company_id), False)
|
||||
if current_value and current_value.state != 'disabled':
|
||||
continue
|
||||
|
||||
acquirers_map[(acquirer.provider, acquirer.company_id)] = acquirer
|
||||
providers_map[(provider.code, provider.company_id)] = provider
|
||||
|
||||
for line in self:
|
||||
code = line.payment_method_id.code
|
||||
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
|
||||
def _get_payment_method_domain(self, code):
|
||||
@@ -46,19 +46,19 @@ class AccountPaymentMethodLine(models.Model):
|
||||
|
||||
unique = information.get('mode') == '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:
|
||||
domain = expression.AND([domain, [('company_id', 'in', company_ids.ids)]])
|
||||
|
||||
return domain
|
||||
|
||||
def action_open_acquirer_form(self):
|
||||
def action_open_provider_form(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Acquirer'),
|
||||
'name': _('Provider'),
|
||||
'view_mode': 'form',
|
||||
'res_model': 'payment.acquirer',
|
||||
'res_model': 'payment.provider',
|
||||
'target': 'current',
|
||||
'res_id': self.payment_acquirer_id.id
|
||||
'res_id': self.payment_provider_id.id
|
||||
}
|
||||
|
||||
+32
-32
@@ -3,8 +3,8 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class PaymentAcquirer(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
class Paymentprovider(models.Model):
|
||||
_inherit = 'payment.provider'
|
||||
|
||||
journal_id = fields.Many2one(
|
||||
string="Payment Journal",
|
||||
@@ -17,74 +17,74 @@ class PaymentAcquirer(models.Model):
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
@api.depends('provider')
|
||||
@api.depends('code')
|
||||
def _compute_journal_id(self):
|
||||
for acquirer in self:
|
||||
for provider in self:
|
||||
payment_method = self.env['account.payment.method.line'].search([
|
||||
('journal_id.company_id', '=', acquirer.company_id.id),
|
||||
('code', '=', acquirer.provider)
|
||||
('journal_id.company_id', '=', provider.company_id.id),
|
||||
('code', '=', provider.code)
|
||||
], limit=1)
|
||||
if payment_method:
|
||||
acquirer.journal_id = payment_method.journal_id
|
||||
provider.journal_id = payment_method.journal_id
|
||||
else:
|
||||
acquirer.journal_id = False
|
||||
provider.journal_id = False
|
||||
|
||||
def _inverse_journal_id(self):
|
||||
for acquirer in self:
|
||||
for provider in self:
|
||||
payment_method_line = self.env['account.payment.method.line'].search([
|
||||
('journal_id.company_id', '=', acquirer.company_id.id),
|
||||
('code', '=', acquirer.provider),
|
||||
('journal_id.company_id', '=', provider.company_id.id),
|
||||
('code', '=', provider.code),
|
||||
], limit=1)
|
||||
if acquirer.journal_id:
|
||||
if provider.journal_id:
|
||||
if not payment_method_line:
|
||||
default_payment_method_id = acquirer._get_default_payment_method_id(
|
||||
acquirer.provider
|
||||
default_payment_method_id = provider._get_default_payment_method_id(
|
||||
provider.code
|
||||
)
|
||||
existing_payment_method_line = self.env['account.payment.method.line'].search([
|
||||
('payment_method_id', '=', default_payment_method_id),
|
||||
('journal_id', '=', acquirer.journal_id.id),
|
||||
('journal_id', '=', provider.journal_id.id),
|
||||
], limit=1)
|
||||
if not existing_payment_method_line:
|
||||
self.env['account.payment.method.line'].create({
|
||||
'payment_method_id': default_payment_method_id,
|
||||
'journal_id': acquirer.journal_id.id,
|
||||
'journal_id': provider.journal_id.id,
|
||||
})
|
||||
else:
|
||||
payment_method_line.journal_id = acquirer.journal_id
|
||||
payment_method_line.journal_id = provider.journal_id
|
||||
elif payment_method_line:
|
||||
payment_method_line.unlink()
|
||||
|
||||
@api.model
|
||||
def _get_default_payment_method_id(self, provider):
|
||||
provider_payment_method = self._get_provider_payment_method(provider)
|
||||
def _get_default_payment_method_id(self, code):
|
||||
provider_payment_method = self._get_provider_payment_method(code)
|
||||
if provider_payment_method:
|
||||
return provider_payment_method.id
|
||||
return self.env.ref('account.account_payment_method_manual_in').id
|
||||
|
||||
@api.model
|
||||
def _get_provider_payment_method(self, provider):
|
||||
return self.env['account.payment.method'].search([('code', '=', provider)], limit=1)
|
||||
def _get_provider_payment_method(self, code):
|
||||
return self.env['account.payment.method'].search([('code', '=', code)], limit=1)
|
||||
|
||||
#=== BUSINESS METHODS ===#
|
||||
|
||||
@api.model
|
||||
def _setup_provider(self, provider_code):
|
||||
def _setup_provider(self, code):
|
||||
""" Override of `payment` to create the payment method of the provider. """
|
||||
super()._setup_provider(provider_code)
|
||||
self._setup_payment_method(provider_code)
|
||||
super()._setup_provider(code)
|
||||
self._setup_payment_method(code)
|
||||
|
||||
@api.model
|
||||
def _setup_payment_method(self, provider):
|
||||
if provider not in ('none', 'transfer') and not self._get_provider_payment_method(provider):
|
||||
providers_description = dict(self._fields['provider']._description_selection(self.env))
|
||||
def _setup_payment_method(self, code):
|
||||
if code not in ('none', 'transfer') and not self._get_provider_payment_method(code):
|
||||
providers_description = dict(self._fields['code']._description_selection(self.env))
|
||||
self.env['account.payment.method'].create({
|
||||
'name': providers_description[provider],
|
||||
'code': provider,
|
||||
'name': providers_description[code],
|
||||
'code': code,
|
||||
'payment_type': 'inbound',
|
||||
})
|
||||
|
||||
@api.model
|
||||
def _remove_provider(self, provider):
|
||||
def _remove_provider(self, code):
|
||||
""" Override of `payment` to delete the payment method of the provider. """
|
||||
super()._remove_provider(provider)
|
||||
self._get_provider_payment_method(provider).unlink()
|
||||
super()._remove_provider(code)
|
||||
self._get_provider_payment_method(code).unlink()
|
||||
@@ -64,7 +64,7 @@ class PaymentTransaction(models.Model):
|
||||
#=== BUSINESS METHODS - PAYMENT FLOW ===#
|
||||
|
||||
@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.
|
||||
|
||||
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, ...).
|
||||
|
||||
: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 dict values: The transaction values used to compute the reference prefix. It should
|
||||
have the structure {'invoice_ids': [(X2M command), ...], ...}.
|
||||
@@ -87,7 +87,7 @@ class PaymentTransaction(models.Model):
|
||||
invoices = self.env['account.move'].browse(invoice_ids).exists()
|
||||
if len(invoices) == len(invoice_ids): # All ids are valid
|
||||
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):
|
||||
""" Update the transactions' state to 'cancel'.
|
||||
@@ -134,16 +134,16 @@ class PaymentTransaction(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
payment_method_line = self.acquirer_id.journal_id.inbound_payment_method_line_ids\
|
||||
.filtered(lambda l: l.code == self.provider)
|
||||
payment_method_line = self.provider_id.journal_id.inbound_payment_method_line_ids\
|
||||
.filtered(lambda l: l.code == self.provider_code)
|
||||
payment_values = {
|
||||
'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',
|
||||
'currency_id': self.currency_id.id,
|
||||
'partner_id': self.partner_id.commercial_partner_id.id,
|
||||
'partner_type': 'customer',
|
||||
'journal_id': self.acquirer_id.journal_id.id,
|
||||
'company_id': self.acquirer_id.company_id.id,
|
||||
'journal_id': self.provider_id.journal_id.id,
|
||||
'company_id': self.provider_id.company_id.id,
|
||||
'payment_method_line_id': payment_method_line.id,
|
||||
'payment_token_id': self.token_id.id,
|
||||
'payment_transaction_id': self.id,
|
||||
|
||||
@@ -15,12 +15,12 @@ odoo.define('account_payment.payment_form', require => {
|
||||
*
|
||||
* @override method from payment.payment_form_mixin
|
||||
* @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 {string} flow - The online payment flow of the selected payment option.
|
||||
* @return {object} The extended transaction route params.
|
||||
*/
|
||||
_prepareTransactionRouteParams: function (provider, paymentOptionId, flow) {
|
||||
_prepareTransactionRouteParams: function (code, paymentOptionId, flow) {
|
||||
const transactionRouteParams = this._super(...arguments);
|
||||
return {
|
||||
...transactionRouteParams,
|
||||
|
||||
@@ -28,7 +28,7 @@ class AccountPaymentCommon(PaymentCommon, AccountTestInvoicingCommon):
|
||||
'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.invoice = cls.env['account.move'].create({
|
||||
@@ -60,23 +60,23 @@ class AccountPaymentCommon(PaymentCommon, AccountTestInvoicingCommon):
|
||||
#=== Utils ===#
|
||||
|
||||
@classmethod
|
||||
def _prepare_acquirer(cls, provider='none', company=None, update_values=None):
|
||||
""" Override of `payment` to prepare and return the first acquirer matching the given
|
||||
def _prepare_provider(cls, provider_code='none', company=None, update_values=None):
|
||||
""" Override of `payment` to 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.
|
||||
All other acquirers belonging to the same company are disabled to avoid any interferences.
|
||||
If no provider is found in the given company, we duplicate the one from the base company.
|
||||
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 recordset company: The company of the acquirer to prepare, as a `res.company` record.
|
||||
:param dict update_values: The values used to update the acquirer.
|
||||
:return: The acquirer to prepare, if found.
|
||||
:rtype: recordset of `payment.acquirer`
|
||||
:param str provider_code: The code of the provider to prepare.
|
||||
: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 provider.
|
||||
:return: The provider to prepare, if found.
|
||||
:rtype: recordset of `payment.provider`
|
||||
"""
|
||||
acquirer = super()._prepare_acquirer(provider, company, update_values)
|
||||
if not acquirer.journal_id:
|
||||
acquirer.journal_id = cls.env['account.journal'].search(
|
||||
[('company_id', '=', acquirer.company_id.id), ('type', '=', 'bank')],
|
||||
provider = super()._prepare_provider(provider_code, company, update_values)
|
||||
if not provider.journal_id:
|
||||
provider.journal_id = cls.env['account.journal'].search(
|
||||
[('company_id', '=', provider.company_id.id), ('type', '=', 'bank')],
|
||||
limit=1,
|
||||
)
|
||||
return acquirer
|
||||
return provider
|
||||
|
||||
@@ -11,18 +11,18 @@ from odoo.addons.account_payment.tests.common import AccountPaymentCommon
|
||||
class TestAccountPayment(AccountPaymentCommon):
|
||||
|
||||
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._reconcile_after_done() # Create the payment
|
||||
self.assertEqual(
|
||||
tx.payment_id.amount_available_for_refund,
|
||||
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."
|
||||
)
|
||||
|
||||
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._reconcile_after_done() # Create the payment
|
||||
self.assertAlmostEqual(
|
||||
@@ -34,7 +34,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
||||
)
|
||||
|
||||
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_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):
|
||||
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._reconcile_after_done() # Create the payment
|
||||
self._create_transaction(
|
||||
@@ -77,7 +77,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
||||
)
|
||||
|
||||
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._reconcile_after_done() # Create the payment
|
||||
self._create_transaction(
|
||||
@@ -98,7 +98,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
||||
)
|
||||
|
||||
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._reconcile_after_done() # Create the payment
|
||||
for reference_index, operation in enumerate(
|
||||
@@ -127,7 +127,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
||||
'date': '2019-01-01',
|
||||
'currency_id': self.currency.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_with_token = payment_without_token.copy()
|
||||
|
||||
@@ -34,7 +34,7 @@ class TestFlows(AccountPaymentCommon, PaymentHttpCommon):
|
||||
}
|
||||
route_values.update({
|
||||
'flow': 'direct',
|
||||
'payment_option_id': self.acquirer.id,
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
})
|
||||
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="arch" type="xml">
|
||||
<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_acquirer_state" invisible="1"/>
|
||||
<button name="action_open_acquirer_form"
|
||||
<field name="payment_provider_id" invisible="1"/>
|
||||
<field name="payment_provider_state" invisible="1"/>
|
||||
<button name="action_open_provider_form"
|
||||
type="object"
|
||||
string="SETUP"
|
||||
class="float-end btn-secondary"
|
||||
attrs="{'invisible': [('payment_acquirer_id', '=', False)]}"
|
||||
attrs="{'invisible': [('payment_provider_id', '=', False)]}"
|
||||
groups="base.group_system"/>
|
||||
</xpath>
|
||||
<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>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<menuitem action="payment.action_payment_acquirer"
|
||||
id="payment_acquirer_menu"
|
||||
<menuitem action="payment.action_payment_provider"
|
||||
id="payment_provider_menu"
|
||||
parent="account.root_payment_menu"
|
||||
sequence="10"/>
|
||||
<menuitem action="payment.action_payment_icon"
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<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"/>
|
||||
</div>
|
||||
<div t-else="" class="alert alert-warning">
|
||||
@@ -125,8 +125,8 @@
|
||||
<xpath expr="//a[hasclass('close')]" position="after">
|
||||
<t t-if="success == 'pay_invoice'">
|
||||
<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"/>
|
||||
<div t-if="payment_tx_id.acquirer_id.sudo().pending_msg and payment_tx_id.provider == 'transfer' and invoice.ref">
|
||||
<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.provider_id.sudo().pending_msg and payment_tx_id.provider_code == 'transfer' and invoice.ref">
|
||||
<b>Communication: </b><span t-esc='invoice.ref'/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="payment_acquirer_form" model="ir.ui.view">
|
||||
<field name="name">payment.acquirer.form</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
|
||||
<record id="payment_provider_form" model="ir.ui.view">
|
||||
<field name="name">payment.provider.form</field>
|
||||
<field name="model">payment.provider</field>
|
||||
<field name="inherit_id" ref="payment.payment_provider_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="payment_followup" position="attributes">
|
||||
<attribute name="invisible">
|
||||
@@ -14,7 +14,7 @@
|
||||
<group name="payment_followup" position="inside">
|
||||
<field name="journal_id"
|
||||
context="{'default_type': 'bank'}"
|
||||
attrs="{'required': [('state', '!=', 'disabled'), ('provider', 'not in', ['none', 'transfer'])]}"/>
|
||||
attrs="{'required': [('state', '!=', 'disabled'), ('code', 'not in', ['none', 'transfer'])]}"/>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
@@ -13,7 +13,7 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
domain='''[
|
||||
('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.")
|
||||
|
||||
# == Display purpose fields ==
|
||||
@@ -44,9 +44,9 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
|
||||
wizard.suitable_payment_token_ids = self.env['payment.token'].sudo().search([
|
||||
('company_id', '=', wizard.company_id.id),
|
||||
('acquirer_id.capture_manually', '=', False),
|
||||
('provider_id.capture_manually', '=', False),
|
||||
('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:
|
||||
wizard.suitable_payment_token_ids = [Command.clear()]
|
||||
@@ -55,13 +55,13 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
def _compute_use_electronic_payment_method(self):
|
||||
for wizard in self:
|
||||
# Get a list of all electronic payment method codes.
|
||||
# These codes are comprised of the providers of each payment acquirer.
|
||||
codes = [key for key in dict(self.env['payment.acquirer']._fields['provider']._description_selection(self.env))]
|
||||
# These codes are comprised of the codes of each payment provider.
|
||||
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
|
||||
|
||||
@api.onchange('can_edit_wizard', 'payment_method_line_id', 'journal_id')
|
||||
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:
|
||||
related_partner_ids = (
|
||||
wizard.partner_id
|
||||
@@ -76,8 +76,8 @@ class AccountPaymentRegister(models.TransientModel):
|
||||
wizard.payment_token_id = self.env['payment.token'].sudo().search([
|
||||
('company_id', '=', wizard.company_id.id),
|
||||
('partner_id', 'in', related_partner_ids.ids),
|
||||
('acquirer_id.capture_manually', '=', False),
|
||||
('acquirer_id', '=', wizard.payment_method_line_id.payment_acquirer_id.id),
|
||||
('provider_id.capture_manually', '=', False),
|
||||
('provider_id', '=', wizard.payment_method_line_id.payment_provider_id.id),
|
||||
], limit=1)
|
||||
else:
|
||||
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
|
||||
)
|
||||
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(
|
||||
string="Has a pending refund", compute='_compute_has_pending_refund'
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<p>
|
||||
<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
|
||||
few minutes, please check your payment acquirer configuration.
|
||||
few minutes, please check your payment provider configuration.
|
||||
</p>
|
||||
</div>
|
||||
<group>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<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')"/>
|
||||
</record>
|
||||
<function model="payment.acquirer" name="copy">
|
||||
<value eval="[ref('payment.payment_acquirer_transfer')]"/>
|
||||
<function model="payment.provider" name="copy">
|
||||
<value eval="[ref('payment.payment_provider_transfer')]"/>
|
||||
<value eval="{'company_id': ref('l10n_ar.company_ri'), 'state': 'enabled'}"/>
|
||||
</function>
|
||||
<function model="product.pricelist" name="write">
|
||||
|
||||
@@ -8,11 +8,11 @@ from . import wizards
|
||||
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['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['payment.acquirer']._remove_provider(provider)
|
||||
env['payment.provider']._remove_provider(code)
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Payment Acquirer',
|
||||
'name': 'Payment Provider',
|
||||
'version': '2.0',
|
||||
'category': 'Hidden',
|
||||
'summary': 'Base Module for Payment Acquirers',
|
||||
'description': """Payment Acquirer Base Module""",
|
||||
'summary': 'Base Module for Payment Providers',
|
||||
'description': """Payment Provider Base Module""",
|
||||
'depends': ['portal'],
|
||||
'data': [
|
||||
'data/payment_icon_data.xml',
|
||||
'data/payment_acquirer_data.xml',
|
||||
'data/payment_provider_data.xml',
|
||||
'data/payment_cron.xml',
|
||||
|
||||
'views/payment_portal_templates.xml',
|
||||
'views/payment_templates.xml',
|
||||
|
||||
'views/payment_acquirer_views.xml',
|
||||
'views/payment_provider_views.xml',
|
||||
'views/payment_icon_views.xml',
|
||||
'views/payment_transaction_views.xml',
|
||||
'views/payment_token_views.xml', # Depends on `action_payment_transaction_linked_to_token`
|
||||
@@ -43,7 +43,7 @@
|
||||
'payment/static/src/js/post_processing.js',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'payment/static/src/scss/payment_acquirer.scss',
|
||||
'payment/static/src/scss/payment_provider.scss',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
|
||||
@@ -39,7 +39,7 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
)
|
||||
def payment_pay(
|
||||
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.
|
||||
|
||||
@@ -58,7 +58,7 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
: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 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 dict kwargs: Optional data passed to helper methods.
|
||||
:return: The rendered checkout form
|
||||
@@ -66,8 +66,8 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
: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
|
||||
currency_id, acquirer_id, partner_id, company_id = tuple(map(
|
||||
self._cast_as_int, (currency_id, acquirer_id, partner_id, company_id)
|
||||
currency_id, provider_id, partner_id, company_id = tuple(map(
|
||||
self._cast_as_int, (currency_id, provider_id, partner_id, company_id)
|
||||
))
|
||||
amount = self._cast_as_float(amount)
|
||||
|
||||
@@ -110,31 +110,31 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
if not currency or not currency.active:
|
||||
raise werkzeug.exceptions.NotFound # The currency must exist and be active
|
||||
|
||||
# Select all acquirers and tokens that match the constraints
|
||||
acquirers_sudo = request.env['payment.acquirer'].sudo()._get_compatible_acquirers(
|
||||
# Select all providers and tokens that match the constraints
|
||||
providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
|
||||
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)
|
||||
if acquirer_id in acquirers_sudo.ids: # Only keep the desired acquirer if it's suitable
|
||||
acquirers_sudo = acquirers_sudo.browse(acquirer_id)
|
||||
) # In sudo mode to read the fields of providers and partner (if not logged in)
|
||||
if provider_id in providers_sudo.ids: # Only keep the desired provider if it's suitable
|
||||
providers_sudo = providers_sudo.browse(provider_id)
|
||||
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']
|
||||
|
||||
# Compute the fees taken by acquirers supporting the feature
|
||||
fees_by_acquirer = {
|
||||
acq_sudo: acq_sudo._compute_fees(amount, currency, partner_sudo.country_id)
|
||||
for acq_sudo in acquirers_sudo.filtered('fees_active')
|
||||
# Compute the fees taken by providers supporting the feature
|
||||
fees_by_provider = {
|
||||
pro_sudo: pro_sudo._compute_fees(amount, currency, partner_sudo.country_id)
|
||||
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
|
||||
access_token = payment_utils.generate_access_token(partner_sudo.id, amount, currency.id)
|
||||
|
||||
rendering_context = {
|
||||
'acquirers': acquirers_sudo,
|
||||
'providers': providers_sudo,
|
||||
'tokens': payment_tokens,
|
||||
'fees_by_acquirer': fees_by_acquirer,
|
||||
'fees_by_provider': fees_by_provider,
|
||||
'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,
|
||||
'amount': amount,
|
||||
@@ -150,23 +150,23 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
return request.render(self._get_payment_page_template_xmlid(**kwargs), rendering_context)
|
||||
|
||||
@staticmethod
|
||||
def _compute_show_tokenize_input_mapping(acquirers_sudo, logged_in=False, **kwargs):
|
||||
""" Determine for each acquirer whether the tokenization input should be shown or not.
|
||||
def _compute_show_tokenize_input_mapping(providers_sudo, logged_in=False, **kwargs):
|
||||
""" 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
|
||||
`payment.acquirer` recordset.
|
||||
`payment.provider` recordset.
|
||||
:param bool logged_in: Whether the user is logged in or not.
|
||||
: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
|
||||
"""
|
||||
show_tokenize_input_mapping = {}
|
||||
for acquirer_sudo in acquirers_sudo:
|
||||
show_tokenize_input = acquirer_sudo.allow_tokenization \
|
||||
and not acquirer_sudo._is_tokenization_required(**kwargs) \
|
||||
for provider_sudo in providers_sudo:
|
||||
show_tokenize_input = provider_sudo.allow_tokenization \
|
||||
and not provider_sudo._is_tokenization_required(**kwargs) \
|
||||
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
|
||||
|
||||
def _get_payment_page_template_xmlid(self, **kwargs):
|
||||
@@ -181,7 +181,7 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
:rtype: str
|
||||
"""
|
||||
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,
|
||||
partner_sudo.id,
|
||||
0., # There is no amount to pay with validation transactions.
|
||||
@@ -189,15 +189,15 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
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([
|
||||
('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)
|
||||
rendering_context = {
|
||||
'acquirers': acquirers_sudo,
|
||||
'providers': providers_sudo,
|
||||
'tokens': tokens_sudo,
|
||||
'reference_prefix': payment_utils.singularize_reference_prefix(prefix='V'),
|
||||
'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.
|
||||
|
||||
: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 float|None amount: The amount to pay in the given currency.
|
||||
None if in a payment method validation operation
|
||||
@@ -272,13 +272,13 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
"""
|
||||
# Prepare create values
|
||||
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
|
||||
tokenize = bool(
|
||||
# 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
|
||||
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
|
||||
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:
|
||||
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
|
||||
tokenize = False
|
||||
else:
|
||||
@@ -299,18 +299,18 @@ class PaymentPortal(portal.CustomerPortal):
|
||||
)
|
||||
|
||||
reference = request.env['payment.transaction']._compute_reference(
|
||||
acquirer_sudo.provider,
|
||||
provider_sudo.code,
|
||||
prefix=reference_prefix,
|
||||
**(custom_create_values or {}),
|
||||
**kwargs
|
||||
)
|
||||
if is_validation: # Acquirers determine the amount and currency in validation operations
|
||||
amount = acquirer_sudo._get_validation_amount()
|
||||
currency_id = acquirer_sudo._get_validation_currency().id
|
||||
if is_validation: # Providers determine the amount and currency in validation operations
|
||||
amount = provider_sudo._get_validation_amount()
|
||||
currency_id = provider_sudo._get_validation_currency().id
|
||||
|
||||
# Create the transaction
|
||||
tx_sudo = request.env['payment.transaction'].sudo().create({
|
||||
'acquirer_id': acquirer_sudo.id,
|
||||
'provider_id': provider_sudo.id,
|
||||
'reference': reference,
|
||||
'amount': amount,
|
||||
'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
|
||||
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
|
||||
`payment.transaction` record.
|
||||
|
||||
@@ -58,11 +58,11 @@ class PaymentPostProcessing(http.Controller):
|
||||
for tx in monitored_txs:
|
||||
display_message = None
|
||||
if tx.state == 'pending':
|
||||
display_message = tx.acquirer_id.pending_msg
|
||||
display_message = tx.provider_id.pending_msg
|
||||
elif tx.state == 'done':
|
||||
display_message = tx.acquirer_id.done_msg
|
||||
display_message = tx.provider_id.done_msg
|
||||
elif tx.state == 'cancel':
|
||||
display_message = tx.acquirer_id.cancel_msg
|
||||
display_message = tx.provider_id.cancel_msg
|
||||
display_values_list.append({
|
||||
'display_message': display_message,
|
||||
**tx._get_post_processing_values(),
|
||||
|
||||
+15
-15
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="display_as">Credit Card (powered by Adyen)</field>
|
||||
<field name="image_128" type="base64" file="payment_adyen/static/description/icon.png"/>
|
||||
@@ -20,7 +20,7 @@
|
||||
])]"/>
|
||||
</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="display_as">Amazon Payment Services</field>
|
||||
<field name="image_128" type="base64" file="payment_aps/static/description/icon.png"/>
|
||||
@@ -35,7 +35,7 @@
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_asiapay" model="payment.acquirer">
|
||||
<record id="payment_provider_asiapay" model="payment.provider">
|
||||
<field name="name">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"/>
|
||||
@@ -49,7 +49,7 @@
|
||||
])]"/>
|
||||
</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="display_as">Credit Card (powered by Authorize)</field>
|
||||
<field name="image_128"
|
||||
@@ -68,7 +68,7 @@
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_buckaroo" model="payment.acquirer">
|
||||
<record id="payment_provider_buckaroo" model="payment.provider">
|
||||
<field name="name">Buckaroo</field>
|
||||
<field name="display_as">Credit Card (powered by Buckaroo)</field>
|
||||
<field name="image_128"
|
||||
@@ -86,14 +86,14 @@
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_demo" model="payment.acquirer">
|
||||
<record id="payment_provider_demo" model="payment.provider">
|
||||
<field name="name">Demo</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="image_128" type="base64" file="payment_demo/static/description/icon.png"/>
|
||||
<field name="module_id" ref="base.module_payment_demo"/>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_flutterwave" model="payment.acquirer">
|
||||
<record id="payment_provider_flutterwave" model="payment.provider">
|
||||
<field name="name">Flutterwave</field>
|
||||
<field name="image_128"
|
||||
type="base64"
|
||||
@@ -111,7 +111,7 @@
|
||||
])]"/>
|
||||
</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="display_as">Credit Card (powered by Mercado Pago)</field>
|
||||
<field name="image_128"
|
||||
@@ -132,7 +132,7 @@
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_mollie" model="payment.acquirer">
|
||||
<record id="payment_provider_mollie" model="payment.provider">
|
||||
<field name="name">Mollie</field>
|
||||
<field name="image_128" type="base64" file="payment_mollie/static/description/icon.png"/>
|
||||
<field name="module_id" ref="base.module_payment_mollie"/>
|
||||
@@ -155,7 +155,7 @@
|
||||
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_paypal" model="payment.acquirer">
|
||||
<record id="payment_provider_paypal" model="payment.provider">
|
||||
<field name="name">PayPal</field>
|
||||
<field name="image_128" type="base64" file="payment_paypal/static/description/icon.png"/>
|
||||
<field name="module_id" ref="base.module_payment_paypal"/>
|
||||
@@ -173,7 +173,7 @@
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_razorpay" model="payment.acquirer">
|
||||
<record id="payment_provider_razorpay" model="payment.provider">
|
||||
<field name="name">Razorpay</field>
|
||||
<field name="display_as">Credit & Debit Card, UPI (Powered by Razorpay)</field>
|
||||
<field name="image_128" type="base64" file="payment_razorpay/static/description/icon.png"/>
|
||||
@@ -190,7 +190,7 @@
|
||||
])]"/>
|
||||
</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="sequence">20</field>
|
||||
<field name="image_128"
|
||||
@@ -199,7 +199,7 @@
|
||||
<field name="module_id" ref="base.module_payment_sepa_direct_debit"/>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_sips" model="payment.acquirer">
|
||||
<record id="payment_provider_sips" model="payment.provider">
|
||||
<field name="name">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"/>
|
||||
@@ -219,7 +219,7 @@
|
||||
])]"/>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_stripe" model="payment.acquirer">
|
||||
<record id="payment_provider_stripe" model="payment.provider">
|
||||
<field name="name">Stripe</field>
|
||||
<field name="display_as">Credit & Debit Card</field>
|
||||
<field name="image_128" type="base64" file="payment_stripe/static/description/icon.png"/>
|
||||
@@ -240,7 +240,7 @@
|
||||
])]"/>
|
||||
</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="sequence">30</field>
|
||||
<field name="image_128"
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from . import ir_http
|
||||
from . import ir_ui_view
|
||||
from . import payment_acquirer
|
||||
from . import payment_provider
|
||||
from . import payment_icon
|
||||
from . import payment_token
|
||||
from . import payment_transaction
|
||||
|
||||
@@ -8,9 +8,9 @@ class IrUiView(models.Model):
|
||||
_inherit = 'ir.ui.view'
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_if_not_referenced_by_acquirer(self):
|
||||
referencing_acquirers_sudo = self.env['payment.acquirer'].sudo().search([
|
||||
def _unlink_if_not_referenced_by_provider(self):
|
||||
referencing_providers_sudo = self.env['payment.provider'].sudo().search([
|
||||
'|', ('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.
|
||||
if referencing_acquirers_sudo:
|
||||
raise UserError(_("You cannot delete a view that is used by a payment acquirer."))
|
||||
if referencing_providers_sudo:
|
||||
raise UserError(_("You cannot delete a view that is used by a payment provider."))
|
||||
|
||||
@@ -9,9 +9,9 @@ class PaymentIcon(models.Model):
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char(string="Name")
|
||||
acquirer_ids = fields.Many2many(
|
||||
string="Acquirers", comodel_name='payment.acquirer',
|
||||
help="The list of acquirers supporting this payment icon")
|
||||
provider_ids = fields.Many2many(
|
||||
string="Providers", comodel_name='payment.provider',
|
||||
help="The list of providers supporting this payment icon")
|
||||
image = fields.Image(
|
||||
string="Image", max_width=64, max_height=64,
|
||||
help="This field holds the image used for this payment icon, limited to 64x64 px")
|
||||
|
||||
+72
-72
@@ -11,9 +11,9 @@ from odoo.osv import expression
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentAcquirer(models.Model):
|
||||
_name = 'payment.acquirer'
|
||||
_description = 'Payment Acquirer'
|
||||
class PaymentProvider(models.Model):
|
||||
_name = 'payment.provider'
|
||||
_description = 'Payment Provider'
|
||||
_order = 'module_state, state desc, sequence, name'
|
||||
|
||||
def _valid_field_parameter(self, field, name):
|
||||
@@ -22,18 +22,18 @@ class PaymentAcquirer(models.Model):
|
||||
# Configuration fields
|
||||
name = fields.Char(string="Name", required=True, translate=True)
|
||||
sequence = fields.Integer(string="Sequence", help="Define the display order")
|
||||
provider = fields.Selection(
|
||||
string="Provider", help="The Payment Service Provider to use with this acquirer",
|
||||
code = fields.Selection(
|
||||
string="Code", help="The Payment Service Code to use with this provider",
|
||||
selection=[('none', "No Provider Set")], default='none', required=True)
|
||||
state = fields.Selection(
|
||||
string="State",
|
||||
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")],
|
||||
default='disabled', required=True, copy=False)
|
||||
is_published = fields.Boolean(
|
||||
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.",
|
||||
)
|
||||
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",
|
||||
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"
|
||||
"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(
|
||||
string="Capture Amount Manually",
|
||||
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(
|
||||
string="Countries",
|
||||
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.",
|
||||
relation='payment_country_rel',
|
||||
column1='payment_id',
|
||||
@@ -94,7 +94,7 @@ class PaymentAcquirer(models.Model):
|
||||
)
|
||||
maximum_amount = fields.Monetary(
|
||||
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.",
|
||||
currency_field='main_currency_id',
|
||||
)
|
||||
@@ -108,7 +108,7 @@ class PaymentAcquirer(models.Model):
|
||||
|
||||
# Message fields
|
||||
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)
|
||||
pre_msg = fields.Html(
|
||||
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')
|
||||
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
|
||||
"""
|
||||
for acquirer in self:
|
||||
if acquirer.module_id and not acquirer.module_state == 'installed':
|
||||
acquirer.color = 4 # blue
|
||||
elif acquirer.state == 'disabled':
|
||||
acquirer.color = 3 # yellow
|
||||
elif acquirer.state == 'test':
|
||||
acquirer.color = 2 # orange
|
||||
elif acquirer.state == 'enabled':
|
||||
acquirer.color = 7 # green
|
||||
for provider in self:
|
||||
if provider.module_id and not provider.module_state == 'installed':
|
||||
provider.color = 4 # blue
|
||||
elif provider.state == 'disabled':
|
||||
provider.color = 3 # yellow
|
||||
elif provider.state == 'test':
|
||||
provider.color = 2 # orange
|
||||
elif provider.state == 'enabled':
|
||||
provider.color = 7 # green
|
||||
|
||||
@api.depends('provider')
|
||||
@api.depends('code')
|
||||
def _compute_view_configuration_fields(self):
|
||||
""" Compute view configuration fields based on the provider.
|
||||
|
||||
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`.
|
||||
|
||||
:return: None
|
||||
@@ -217,7 +217,7 @@ class PaymentAcquirer(models.Model):
|
||||
def _compute_feature_support_fields(self):
|
||||
""" 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
|
||||
"""
|
||||
@@ -233,7 +233,7 @@ class PaymentAcquirer(models.Model):
|
||||
|
||||
@api.onchange('state')
|
||||
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
|
||||
"""
|
||||
@@ -241,9 +241,9 @@ class PaymentAcquirer(models.Model):
|
||||
|
||||
@api.onchange('state')
|
||||
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.
|
||||
|
||||
: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:
|
||||
related_tokens = self.env['payment.token'].search(
|
||||
[('acquirer_id', '=', self._origin.id)]
|
||||
[('provider_id', '=', self._origin.id)]
|
||||
)
|
||||
if related_tokens:
|
||||
return {
|
||||
@@ -261,7 +261,7 @@ class PaymentAcquirer(models.Model):
|
||||
'title': _("Warning"),
|
||||
'message': _(
|
||||
"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
|
||||
"""
|
||||
for acquirer in self:
|
||||
if any(not 0 <= fee < 100 for fee in (acquirer.fees_dom_var, acquirer.fees_int_var)):
|
||||
for provider in self:
|
||||
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%."))
|
||||
|
||||
#=== CRUD METHODS ===#
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values_list):
|
||||
acquirers = super().create(values_list)
|
||||
acquirers._check_required_if_provider()
|
||||
return acquirers
|
||||
providers = super().create(values_list)
|
||||
providers._check_required_if_provider()
|
||||
return providers
|
||||
|
||||
def write(self, values):
|
||||
# Handle acquirer disabling.
|
||||
# Handle provider disabling.
|
||||
if 'state' in values:
|
||||
state_changed_acquirers = self.filtered(
|
||||
lambda acq: acq.state not in ('disabled', values['state'])
|
||||
) # Don't handle acquirers being enabled or whose state is not updated.
|
||||
state_changed_acquirers._handle_state_change()
|
||||
state_changed_providers = self.filtered(
|
||||
lambda p: p.state not in ('disabled', values['state'])
|
||||
) # Don't handle providers being enabled or whose state is not updated.
|
||||
state_changed_providers._handle_state_change()
|
||||
|
||||
result = super().write(values)
|
||||
self._check_required_if_provider()
|
||||
@@ -303,23 +303,23 @@ class PaymentAcquirer(models.Model):
|
||||
return result
|
||||
|
||||
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
|
||||
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'.
|
||||
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
|
||||
:raise ValidationError: if an acquirer-specific required field is empty
|
||||
:raise ValidationError: if a provider-specific required field is empty
|
||||
"""
|
||||
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():
|
||||
required_provider = getattr(field, 'required_if_provider', None)
|
||||
if required_provider and any(
|
||||
required_provider == acquirer.provider and not acquirer[name]
|
||||
for acquirer in enabled_acquirers
|
||||
required_provider == provider.code and not provider[name]
|
||||
for provider in enabled_providers
|
||||
):
|
||||
ir_field = self.env['ir.model.fields']._get(self._name, name)
|
||||
field_names.append(ir_field.field_description)
|
||||
@@ -329,16 +329,16 @@ class PaymentAcquirer(models.Model):
|
||||
)
|
||||
|
||||
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
|
||||
"""
|
||||
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 ===#
|
||||
|
||||
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()
|
||||
|
||||
@@ -353,40 +353,40 @@ class PaymentAcquirer(models.Model):
|
||||
}
|
||||
|
||||
def action_toggle_is_published(self):
|
||||
""" Toggle the acquirer's is_published state.
|
||||
""" Toggle the provider's is_published state.
|
||||
|
||||
:return: none
|
||||
:raise UserError: If the acquirer is disabled.
|
||||
:raise UserError: If the provider is disabled.
|
||||
"""
|
||||
if self.state != 'disabled':
|
||||
self.is_published = not self.is_published
|
||||
else:
|
||||
raise UserError(_("You cannot publish a disabled acquirer."))
|
||||
raise UserError(_("You cannot publish a disabled provider."))
|
||||
|
||||
#=== BUSINESS METHODS ===#
|
||||
|
||||
@api.model
|
||||
def _get_compatible_acquirers(
|
||||
def _get_compatible_providers(
|
||||
self, company_id, partner_id, amount, currency_id=None, force_tokenization=False,
|
||||
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.
|
||||
|
||||
: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 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 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_validation: Whether the operation is a validation
|
||||
:param dict kwargs: Optional data. This parameter is not used here
|
||||
:return: The compatible acquirers
|
||||
:rtype: recordset of `payment.acquirer`
|
||||
:return: The compatible providers
|
||||
: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)]
|
||||
|
||||
# Handle the is_published state.
|
||||
@@ -427,8 +427,8 @@ class PaymentAcquirer(models.Model):
|
||||
if is_express_checkout:
|
||||
domain = expression.AND([domain, [('allow_express_checkout', '=', True)]])
|
||||
|
||||
compatible_acquirers = self.env['payment.acquirer'].search(domain)
|
||||
return compatible_acquirers
|
||||
compatible_providers = self.env['payment.provider'].search(domain)
|
||||
return compatible_providers
|
||||
|
||||
def _is_tokenization_required(self, **kwargs):
|
||||
""" 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):
|
||||
""" 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
|
||||
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)
|
||||
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.
|
||||
|
||||
:param float amount: The amount to pay for the transaction
|
||||
@@ -490,7 +490,7 @@ class PaymentAcquirer(models.Model):
|
||||
def _get_validation_amount(self):
|
||||
""" 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
|
||||
null*.
|
||||
|
||||
@@ -505,7 +505,7 @@ class PaymentAcquirer(models.Model):
|
||||
def _get_validation_currency(self):
|
||||
""" 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
|
||||
not null*.
|
||||
|
||||
@@ -520,7 +520,7 @@ class PaymentAcquirer(models.Model):
|
||||
def _get_redirect_form_view(self, is_validation=False):
|
||||
""" 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.
|
||||
|
||||
Note: self.ensure_one()
|
||||
@@ -550,8 +550,8 @@ class PaymentAcquirer(models.Model):
|
||||
:param str provider_code: The code of the provider to setup.
|
||||
:return: None
|
||||
"""
|
||||
acquirers = self.search([('provider', '=', provider_code)])
|
||||
acquirers.write({
|
||||
providers = self.search([('provider', '=', provider_code)])
|
||||
providers.write({
|
||||
'provider': 'none',
|
||||
'state': 'disabled',
|
||||
})
|
||||
@@ -561,7 +561,7 @@ class PaymentAcquirer(models.Model):
|
||||
self.flush_model()
|
||||
self.invalidate_model()
|
||||
self.env.cr.execute("""
|
||||
UPDATE payment_acquirer SET state = 'disabled'
|
||||
UPDATE payment_provider SET state = 'disabled'
|
||||
WHERE state NOT IN ('test', 'disabled')
|
||||
""")
|
||||
|
||||
@@ -573,8 +573,8 @@ class PaymentAcquirer(models.Model):
|
||||
self.flush_model()
|
||||
self.invalidate_model()
|
||||
query = sql.SQL("""
|
||||
UPDATE payment_acquirer
|
||||
UPDATE payment_provider
|
||||
SET ({fields}) = ROW({vals})
|
||||
WHERE provider = %s
|
||||
WHERE code = %s
|
||||
""").format(fields=sql.SQL(','.join(fields)), vals=sql.SQL(', '.join(['NULL'] * len(fields))))
|
||||
self.env.cr.execute(query, (provider, ))
|
||||
@@ -11,18 +11,18 @@ class PaymentToken(models.Model):
|
||||
_order = 'partner_id, id desc'
|
||||
_description = 'Payment Token'
|
||||
|
||||
acquirer_id = fields.Many2one(
|
||||
string="Acquirer Account", comodel_name='payment.acquirer', required=True)
|
||||
provider = fields.Selection(related='acquirer_id.provider')
|
||||
provider_id = fields.Many2one(
|
||||
string="provider Account", comodel_name='payment.provider', required=True)
|
||||
provider_code = fields.Selection(related='provider_id.code')
|
||||
payment_details = fields.Char(
|
||||
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)
|
||||
company_id = fields.Many2one( # Indexed to speed-up ORM searches (from ir_rule or others)
|
||||
related='acquirer_id.company_id', store=True, index=True)
|
||||
acquirer_ref = fields.Char(
|
||||
string="Acquirer Reference", help="The acquirer reference of the token of the transaction",
|
||||
required=True) # This is not the same thing as the acquirer reference of the transaction
|
||||
related='provider_id.company_id', store=True, index=True)
|
||||
provider_ref = fields.Char(
|
||||
string="Provider Reference", help="The provider reference of the token of the transaction",
|
||||
required=True) # This is not the same thing as the provider reference of the transaction
|
||||
transaction_ids = fields.One2many(
|
||||
string="Payment Transactions", comodel_name='payment.transaction', inverse_name='token_id')
|
||||
verified = fields.Boolean(string="Verified")
|
||||
@@ -33,27 +33,27 @@ class PaymentToken(models.Model):
|
||||
@api.model_create_multi
|
||||
def create(self, values_list):
|
||||
for values in values_list:
|
||||
if 'acquirer_id' in values:
|
||||
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
|
||||
if 'provider_id' in values:
|
||||
provider = self.env['payment.provider'].browse(values['provider_id'])
|
||||
|
||||
# Include acquirer-specific create values
|
||||
values.update(self._get_specific_create_values(acquirer.provider, values))
|
||||
# Include provider-specific create values
|
||||
values.update(self._get_specific_create_values(provider.code, values))
|
||||
else:
|
||||
pass # Let psycopg warn about the missing required field
|
||||
|
||||
return super().create(values_list)
|
||||
|
||||
@api.model
|
||||
def _get_specific_create_values(self, provider, values):
|
||||
""" Complete the values of the `create` method with acquirer-specific values.
|
||||
def _get_specific_create_values(self, provider_code, 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
|
||||
dict of values. Acquirer-specific values take precedence over those of the dict of generic
|
||||
For a provider to add its own create values, it must overwrite this method and return a
|
||||
dict of values. Provider-specific values take precedence over those of the dict of generic
|
||||
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
|
||||
:return: The dict of acquirer-specific create values
|
||||
:return: The dict of provider-specific create values
|
||||
:rtype: dict
|
||||
"""
|
||||
return dict()
|
||||
|
||||
@@ -29,27 +29,27 @@ class PaymentTransaction(models.Model):
|
||||
def _lang_get(self):
|
||||
return self.env['res.lang'].get_installed()
|
||||
|
||||
acquirer_id = fields.Many2one(
|
||||
string="Acquirer", comodel_name='payment.acquirer', readonly=True, required=True)
|
||||
provider = fields.Selection(related='acquirer_id.provider')
|
||||
provider_id = fields.Many2one(
|
||||
string="Provider", comodel_name='payment.provider', readonly=True, required=True)
|
||||
provider_code = fields.Selection(related='provider_id.code')
|
||||
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(
|
||||
string="Reference", help="The internal reference of the transaction", readonly=True,
|
||||
required=True) # Already has an index from the UNIQUE SQL constraint
|
||||
acquirer_reference = fields.Char(
|
||||
string="Acquirer Reference", help="The acquirer reference of the transaction",
|
||||
readonly=True) # This is not the same thing as the acquirer reference of the token
|
||||
provider_reference = fields.Char(
|
||||
string="Provider Reference", help="The provider reference of the transaction",
|
||||
readonly=True) # This is not the same thing as the provider reference of the token
|
||||
amount = fields.Monetary(
|
||||
string="Amount", currency_field='currency_id', readonly=True, required=True)
|
||||
currency_id = fields.Many2one(
|
||||
string="Currency", comodel_name='res.currency', readonly=True, required=True)
|
||||
fees = fields.Monetary(
|
||||
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(
|
||||
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(
|
||||
string="Status",
|
||||
selection=[('draft', "Draft"), ('pending', "Pending"), ('authorized', "Authorized"),
|
||||
@@ -145,12 +145,12 @@ class PaymentTransaction(models.Model):
|
||||
def _check_state_authorized_supported(self):
|
||||
""" Check that authorization is supported for a transaction in the 'authorized' state. """
|
||||
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:
|
||||
raise ValidationError(_(
|
||||
"Transaction authorization is not supported by the following payment acquirers: %s",
|
||||
', '.join(set(illegal_authorize_state_txs.mapped('acquirer_id.name')))
|
||||
"Transaction authorization is not supported by the following payment providers: %s",
|
||||
', '.join(set(illegal_authorize_state_txs.mapped('provider_id.name')))
|
||||
))
|
||||
|
||||
@api.constrains('token_id')
|
||||
@@ -164,10 +164,10 @@ class PaymentTransaction(models.Model):
|
||||
@api.model_create_multi
|
||||
def create(self, 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'):
|
||||
values['reference'] = self._compute_reference(acquirer.provider, **values)
|
||||
values['reference'] = self._compute_reference(provider.code, **values)
|
||||
|
||||
# Duplicate partner values
|
||||
partner = self.env['res.partner'].browse(values['partner_id'])
|
||||
@@ -190,12 +190,12 @@ class PaymentTransaction(models.Model):
|
||||
values['fees'] = 0
|
||||
else:
|
||||
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,
|
||||
)
|
||||
|
||||
# Include acquirer-specific create values
|
||||
values.update(self._get_specific_create_values(acquirer.provider, values))
|
||||
# Include provider-specific create 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
|
||||
values['callback_hash'] = self._generate_callback_hash(
|
||||
@@ -219,16 +219,16 @@ class PaymentTransaction(models.Model):
|
||||
return txs
|
||||
|
||||
@api.model
|
||||
def _get_specific_create_values(self, provider, values):
|
||||
""" Complete the values of the `create` method with acquirer-specific values.
|
||||
def _get_specific_create_values(self, provider_code, 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
|
||||
dict of values. Acquirer-specific values take precedence over those of the dict of generic
|
||||
For a provider to add its own create values, it must overwrite this method and return a
|
||||
dict of values. Provider-specific values take precedence over those of the dict of generic
|
||||
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
|
||||
:return: The dict of acquirer-specific create values
|
||||
:return: The dict of provider-specific create values
|
||||
:rtype: dict
|
||||
"""
|
||||
return dict()
|
||||
@@ -268,7 +268,7 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
payment_utils.check_rights_on_recordset(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()
|
||||
|
||||
def action_void(self):
|
||||
@@ -278,7 +278,7 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
payment_utils.check_rights_on_recordset(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()
|
||||
|
||||
def action_refund(self, amount_to_refund=None):
|
||||
@@ -296,7 +296,7 @@ class PaymentTransaction(models.Model):
|
||||
#=== BUSINESS METHODS - PAYMENT FLOW ===#
|
||||
|
||||
@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.
|
||||
|
||||
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
|
||||
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 separator: The custom separator used to separate the prefix from the suffix, and
|
||||
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 ; ...)
|
||||
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
|
||||
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
|
||||
prefix = payment_utils.singularize_reference_prefix()
|
||||
|
||||
@@ -372,12 +372,12 @@ class PaymentTransaction(models.Model):
|
||||
return reference
|
||||
|
||||
@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.
|
||||
|
||||
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 dict values: The transaction values used to compute the reference prefix.
|
||||
:return: an empty string
|
||||
@@ -409,13 +409,13 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
The returned dict contains the following entries:
|
||||
- tx_id: The transaction, as a `payment.transaction` id
|
||||
- acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id
|
||||
- provider: The provider of the acquirer
|
||||
- provider_id: The provider handling the transaction, as a `payment.provider` id
|
||||
- provider_code: The code of the provider
|
||||
- reference: The reference of the transaction
|
||||
- amount: The rounded amount of the transaction
|
||||
- currency_id: The currency of the transaction, as a res.currency 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()
|
||||
|
||||
@@ -426,31 +426,31 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
processing_values = {
|
||||
'tx_id': self.id,
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'provider': self.provider,
|
||||
'provider_id': self.provider_id.id,
|
||||
'provider_code': self.provider_code,
|
||||
'reference': self.reference,
|
||||
'amount': self.amount,
|
||||
'currency_id': self.currency_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))
|
||||
_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': self.reference, 'values': pprint.pformat(processing_values)},
|
||||
)
|
||||
|
||||
# Render the html form for the redirect flow if available
|
||||
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'
|
||||
)
|
||||
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)
|
||||
_logger.info(
|
||||
"acquirer-specific rendering values for transaction with reference "
|
||||
"provider-specific rendering values for transaction with reference "
|
||||
"%(ref)s:\n%(values)s",
|
||||
{'ref': self.reference, 'values': pprint.pformat(rendering_values)},
|
||||
)
|
||||
@@ -460,36 +460,36 @@ class PaymentTransaction(models.Model):
|
||||
return 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
|
||||
dict of acquirer-specific values based on the generic values returned by this method.
|
||||
Acquirer-specific values take precedence over those of the dict of generic processing
|
||||
For a provider to add its own processing values, it must overwrite this method and return a
|
||||
dict of provider-specific values based on the generic values returned by this method.
|
||||
Provider-specific values take precedence over those of the dict of generic processing
|
||||
values.
|
||||
|
||||
: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
|
||||
"""
|
||||
return dict()
|
||||
|
||||
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
|
||||
dict of acquirer-specific values based on the processing values (acquirer-specific
|
||||
For a provider to add its own rendering values, it must overwrite this method and return a
|
||||
dict of provider-specific values based on the processing values (provider-specific
|
||||
processing values included).
|
||||
|
||||
: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
|
||||
"""
|
||||
return dict()
|
||||
|
||||
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.
|
||||
|
||||
Note: self.ensure_one()
|
||||
@@ -497,13 +497,13 @@ class PaymentTransaction(models.Model):
|
||||
:return: None
|
||||
"""
|
||||
self.ensure_one()
|
||||
self._ensure_acquirer_is_not_disabled()
|
||||
self._ensure_provider_is_not_disabled()
|
||||
self._log_sent_message()
|
||||
|
||||
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.
|
||||
|
||||
Note: self.ensure_one()
|
||||
@@ -514,7 +514,7 @@ class PaymentTransaction(models.Model):
|
||||
:rtype: recordset of `payment.transaction`
|
||||
"""
|
||||
self.ensure_one()
|
||||
self._ensure_acquirer_is_not_disabled()
|
||||
self._ensure_provider_is_not_disabled()
|
||||
|
||||
if create_refund_transaction:
|
||||
refund_tx = self._create_refund_transaction(amount_to_refund=amount_to_refund)
|
||||
@@ -534,8 +534,8 @@ class PaymentTransaction(models.Model):
|
||||
self.ensure_one()
|
||||
|
||||
return self.create({
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'reference': self._compute_reference(self.provider, prefix=f'R-{self.reference}'),
|
||||
'provider_id': self.provider_id.id,
|
||||
'reference': self._compute_reference(self.provider_code, prefix=f'R-{self.reference}'),
|
||||
'amount': -(amount_to_refund or self.amount),
|
||||
'currency_id': self.currency_id.id,
|
||||
'token_id': self.token_id.id,
|
||||
@@ -546,9 +546,9 @@ class PaymentTransaction(models.Model):
|
||||
})
|
||||
|
||||
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.
|
||||
|
||||
Note: self.ensure_one()
|
||||
@@ -556,12 +556,12 @@ class PaymentTransaction(models.Model):
|
||||
:return: None
|
||||
"""
|
||||
self.ensure_one()
|
||||
self._ensure_acquirer_is_not_disabled()
|
||||
self._ensure_provider_is_not_disabled()
|
||||
|
||||
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.
|
||||
|
||||
Note: self.ensure_one()
|
||||
@@ -569,40 +569,40 @@ class PaymentTransaction(models.Model):
|
||||
:return: None
|
||||
"""
|
||||
self.ensure_one()
|
||||
self._ensure_acquirer_is_not_disabled()
|
||||
self._ensure_provider_is_not_disabled()
|
||||
|
||||
def _ensure_acquirer_is_not_disabled(self):
|
||||
""" Ensure that the acquirer's state is not 'disabled' before sending a request to its
|
||||
def _ensure_provider_is_not_disabled(self):
|
||||
""" Ensure that the provider's state is not 'disabled' before sending a request to its
|
||||
provider.
|
||||
|
||||
: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(_(
|
||||
"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.
|
||||
|
||||
: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
|
||||
:return: The 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._execute_callback()
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
: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
|
||||
:return: The transaction if found
|
||||
:rtype: recordset of `payment.transaction`
|
||||
@@ -610,12 +610,12 @@ class PaymentTransaction(models.Model):
|
||||
return self
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
Note: self.ensure_one()
|
||||
@@ -799,12 +799,12 @@ class PaymentTransaction(models.Model):
|
||||
def _get_post_processing_values(self):
|
||||
""" 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
|
||||
return a dict of values. Acquirer-specific values take precedence over those of the dict of
|
||||
For a provider to handle transaction status display, it must override this method and
|
||||
return a dict of values. Provider-specific values take precedence over those of the dict of
|
||||
generic post-processing values.
|
||||
|
||||
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
|
||||
- amount: The rounded amount of the transaction
|
||||
- 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
|
||||
- is_post_processed: Whether the transaction has already been post-processed
|
||||
- landing_route: The route the user is redirected to after the transaction
|
||||
- Additional acquirer-specific entries
|
||||
- Additional provider-specific entries
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
@@ -822,7 +822,7 @@ class PaymentTransaction(models.Model):
|
||||
self.ensure_one()
|
||||
|
||||
post_processing_values = {
|
||||
'provider': self.provider,
|
||||
'provider_code': self.provider_code,
|
||||
'reference': self.reference,
|
||||
'amount': self.amount,
|
||||
'currency_code': self.currency_id.name,
|
||||
@@ -832,8 +832,8 @@ class PaymentTransaction(models.Model):
|
||||
'landing_route': self.landing_route,
|
||||
}
|
||||
_logger.debug(
|
||||
"post-processing values of transaction with reference %s for acquirer with id %s:\n%s",
|
||||
self.reference, self.acquirer_id.id, pprint.pformat(post_processing_values)
|
||||
"post-processing values of transaction with reference %s for provider with id %s:\n%s",
|
||||
self.reference, self.provider_id.id, pprint.pformat(post_processing_values)
|
||||
) # DEBUG level because this can get spammy with transactions in non-final states
|
||||
return post_processing_values
|
||||
|
||||
@@ -904,7 +904,7 @@ class PaymentTransaction(models.Model):
|
||||
def _log_received_message(self):
|
||||
""" 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.
|
||||
|
||||
:return: None
|
||||
@@ -941,30 +941,30 @@ class PaymentTransaction(models.Model):
|
||||
# Choose the message based on the payment flow
|
||||
if self.operation in ('online_redirect', 'online_direct'):
|
||||
message = _(
|
||||
"A transaction with reference %(ref)s has been initiated (%(acq_name)s).",
|
||||
ref=self.reference, acq_name=self.acquirer_id.name
|
||||
"A transaction with reference %(ref)s has been initiated (%(provider_name)s).",
|
||||
ref=self.reference, provider_name=self.provider_id.name
|
||||
)
|
||||
elif self.operation == 'refund':
|
||||
formatted_amount = format_amount(self.env, -self.amount, self.currency_id)
|
||||
message = _(
|
||||
"A refund request of %(amount)s has been sent. The payment will be created soon. "
|
||||
"Refund transaction reference: %(ref)s (%(acq_name)s).",
|
||||
amount=formatted_amount, ref=self.reference, acq_name=self.acquirer_id.name
|
||||
"Refund transaction reference: %(ref)s (%(provider_name)s).",
|
||||
amount=formatted_amount, ref=self.reference, provider_name=self.provider_id.name
|
||||
)
|
||||
elif self.operation in ('online_token', 'offline'):
|
||||
message = _(
|
||||
"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,
|
||||
token=self.token_id._build_display_name(),
|
||||
acq_name=self.acquirer_id.name
|
||||
provider_name=self.provider_id.name
|
||||
)
|
||||
else: # 'validation'
|
||||
message = _(
|
||||
"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,
|
||||
acq_name=self.acquirer_id.name,
|
||||
provider_name=self.provider_id.name,
|
||||
)
|
||||
return message
|
||||
|
||||
@@ -978,33 +978,39 @@ class PaymentTransaction(models.Model):
|
||||
formatted_amount = format_amount(self.env, self.amount, self.currency_id)
|
||||
if self.state == 'pending':
|
||||
message = _(
|
||||
"The transaction with reference %(ref)s for %(amount)s is pending (%(acq_name)s).",
|
||||
ref=self.reference, amount=formatted_amount, acq_name=self.acquirer_id.name
|
||||
("The transaction with reference %(ref)s for %(amount)s "
|
||||
"is pending (%(provider_name)s)."),
|
||||
ref=self.reference,
|
||||
amount=formatted_amount,
|
||||
provider_name=self.provider_id.name
|
||||
)
|
||||
elif self.state == 'authorized':
|
||||
message = _(
|
||||
"The transaction with reference %(ref)s for %(amount)s has been authorized "
|
||||
"(%(acq_name)s).", 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
|
||||
)
|
||||
elif self.state == 'done':
|
||||
message = _(
|
||||
"The transaction with reference %(ref)s for %(amount)s has been confirmed "
|
||||
"(%(acq_name)s).", 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
|
||||
)
|
||||
elif self.state == 'error':
|
||||
message = _(
|
||||
"The transaction with reference %(ref)s for %(amount)s encountered an error"
|
||||
" (%(acq_name)s).",
|
||||
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:
|
||||
message += "<br />" + _("Error: %s", self.state_message)
|
||||
else:
|
||||
message = _(
|
||||
"The transaction with reference %(ref)s for %(amount)s is canceled (%(acq_name)s).",
|
||||
ref=self.reference, amount=formatted_amount, acq_name=self.acquirer_id.name
|
||||
("The transaction with reference %(ref)s for %(amount)s is canceled "
|
||||
"(%(provider_name)s)."),
|
||||
ref=self.reference,
|
||||
amount=formatted_amount,
|
||||
provider_name=self.provider_id.name
|
||||
)
|
||||
if self.state_message:
|
||||
message += "<br />" + _("Reason: %s", self.state_message)
|
||||
|
||||
@@ -6,8 +6,8 @@ from odoo import api, fields, models
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
payment_acquirer_onboarding_state = fields.Selection(
|
||||
string="State of the onboarding payment acquirer step",
|
||||
payment_provider_onboarding_state = fields.Selection(
|
||||
string="State of the onboarding payment provider step",
|
||||
selection=[('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done")],
|
||||
default='not_done')
|
||||
payment_onboarding_payment_method = fields.Selection(
|
||||
@@ -20,7 +20,7 @@ class ResCompany(models.Model):
|
||||
])
|
||||
|
||||
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.
|
||||
|
||||
@@ -41,18 +41,18 @@ class ResCompany(models.Model):
|
||||
)
|
||||
|
||||
# Configure Stripe
|
||||
stripe_acquirer = new_env.ref('payment.payment_acquirer_stripe')
|
||||
stripe_acquirer.journal_id = stripe_acquirer.journal_id or default_journal
|
||||
if stripe_acquirer.state == 'disabled': # The onboarding step has never been run
|
||||
stripe_provider = new_env.ref('payment.payment_provider_stripe')
|
||||
stripe_provider.journal_id = stripe_provider.journal_id or default_journal
|
||||
if stripe_provider.state == 'disabled': # The onboarding step has never been run
|
||||
# Configure PayPal
|
||||
paypal_acquirer = new_env.ref('payment.payment_acquirer_paypal')
|
||||
if not paypal_acquirer.paypal_email_account:
|
||||
paypal_acquirer.paypal_email_account = new_env.user.email or new_env.company.email
|
||||
if paypal_acquirer.state == 'disabled' and paypal_acquirer.paypal_email_account:
|
||||
paypal_acquirer.state = 'enabled'
|
||||
paypal_acquirer.journal_id = paypal_acquirer.journal_id or default_journal
|
||||
paypal_provider = new_env.ref('payment.payment_provider_paypal')
|
||||
if not paypal_provider.paypal_email_account:
|
||||
paypal_provider.paypal_email_account = new_env.user.email or new_env.company.email
|
||||
if paypal_provider.state == 'disabled' and paypal_provider.paypal_email_account:
|
||||
paypal_provider.state = 'enabled'
|
||||
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):
|
||||
modules_sudo = self.env['ir.module.module'].sudo().search([('name', 'in', module_names)])
|
||||
@@ -64,9 +64,9 @@ class ResCompany(models.Model):
|
||||
|
||||
: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):
|
||||
""" Override of account. """
|
||||
steps = super().get_account_invoice_onboarding_steps_states_names()
|
||||
return steps + ['payment_acquirer_onboarding_state']
|
||||
return steps + ['payment_provider_onboarding_state']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
payment_acquirer_onboarding_wizard,payment.acquirer.onboarding.wizard,model_payment_acquirer_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_onboarding_wizard,payment.provider.onboarding.wizard,model_payment_provider_onboarding_wizard,base.group_system,1,1,1,0
|
||||
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_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
|
||||
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<!-- Acquirers -->
|
||||
<!-- Providers -->
|
||||
|
||||
<record id="payment_acquirer_company_rule" model="ir.rule">
|
||||
<field name="name">Access acquirers in own companies only</field>
|
||||
<field name="model_id" ref="payment.model_payment_acquirer"/>
|
||||
<record id="payment_provider_company_rule" model="ir.rule">
|
||||
<field name="name">Access providers in own companies only</field>
|
||||
<field name="model_id" ref="payment.model_payment_provider"/>
|
||||
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
|
||||
</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
|
||||
if (flow !== 'token') {
|
||||
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"]');
|
||||
this.txContext.tokenizationRequested = $tokenizeCheckbox.length === 1
|
||||
&& $tokenizeCheckbox[0].checked;
|
||||
|
||||
@@ -48,31 +48,31 @@ publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({
|
||||
_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
|
||||
* @param {Object} acquirerData - The acquirer-specific data.
|
||||
* @param {Object} providerData - The provider-specific data.
|
||||
* @return {Promise}
|
||||
*/
|
||||
async _prepareExpressCheckoutForm(acquirerData) {
|
||||
async _prepareExpressCheckoutForm(providerData) {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
_prepareTransactionRouteParams(acquirerId) {
|
||||
_prepareTransactionRouteParams(providerId) {
|
||||
return {
|
||||
'payment_option_id': parseInt(acquirerId),
|
||||
'payment_option_id': parseInt(providerId),
|
||||
'reference_prefix': this.txContext.referencePrefix &&
|
||||
this.txContent.referencePrefix.toString(),
|
||||
'currency_id': this.txContext.currencyId &&
|
||||
@@ -107,15 +107,15 @@ publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({
|
||||
/**
|
||||
* 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
|
||||
* @param {Object} acquirerData - The acquirer-specific data.
|
||||
* @param {Object} providerData - The provider-specific data.
|
||||
* @param {number} newAmount - The new amount.
|
||||
* @param {number} newMinorAmount - The new minor amount.
|
||||
* @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._enableButton();
|
||||
} 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) {
|
||||
this._hideInlineForms(); // Collapse previously opened inline forms
|
||||
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
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
* whether the payment option is a token. If not, the transaction context is looked up to
|
||||
* determine whether the flow is 'direct' or 'redirect'.
|
||||
@@ -208,7 +208,7 @@ odoo.define('payment.payment_form_mixin', require => {
|
||||
*
|
||||
* @private
|
||||
* @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.
|
||||
*/
|
||||
_getPaymentOptionIdFromRadio: radio => $(radio).data('payment-option-id'),
|
||||
@@ -223,7 +223,7 @@ odoo.define('payment.payment_form_mixin', require => {
|
||||
_getProviderFromRadio: radio => $(radio).data('provider'),
|
||||
|
||||
/**
|
||||
* Remove the error in the acquirer form.
|
||||
* Remove the error in the provider form.
|
||||
*
|
||||
* @private
|
||||
* @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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @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 {string} flow - The online payment flow of the selected payment option
|
||||
* @return {object} The transaction route params
|
||||
*/
|
||||
_prepareTransactionRouteParams: function (provider, paymentOptionId, flow) {
|
||||
_prepareTransactionRouteParams: function (code, paymentOptionId, flow) {
|
||||
return {
|
||||
'payment_option_id': paymentOptionId,
|
||||
'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
|
||||
* inline form. Otherwise, the call must be sent back to the parent method.
|
||||
*
|
||||
* @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 {string} flow - The online payment flow of the selected payment option
|
||||
* @return {Promise}
|
||||
*/
|
||||
_prepareInlineForm: (provider, paymentOptionId, flow) => Promise.resolve(),
|
||||
_prepareInlineForm: (code, paymentOptionId, flow) => Promise.resolve(),
|
||||
|
||||
/**
|
||||
* 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,
|
||||
* it must override this method.
|
||||
* If only post-processing work is needed, an override of `_processRedirectPayment`,
|
||||
* `_processDirectPayment` or `_processTokenPayment` might be more appropriate.
|
||||
*
|
||||
* @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 {string} flow - The online payment flow of the transaction
|
||||
* @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
|
||||
return this._rpc({
|
||||
route: this.txContext.transactionRoute,
|
||||
params: this._prepareTransactionRouteParams(provider, paymentOptionId, flow),
|
||||
params: this._prepareTransactionRouteParams(code, paymentOptionId, flow),
|
||||
}).then(processingValues => {
|
||||
if (flow === 'redirect') {
|
||||
return this._processRedirectPayment(
|
||||
provider, paymentOptionId, processingValues
|
||||
code, paymentOptionId, processingValues
|
||||
);
|
||||
} else if (flow === 'direct') {
|
||||
return this._processDirectPayment(provider, paymentOptionId, processingValues);
|
||||
return this._processDirectPayment(code, paymentOptionId, processingValues);
|
||||
} else if (flow === 'token') {
|
||||
return this._processTokenPayment(provider, paymentOptionId, processingValues);
|
||||
return this._processTokenPayment(code, paymentOptionId, processingValues);
|
||||
}
|
||||
}).guardedCatch(error => {
|
||||
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.
|
||||
*
|
||||
* @private
|
||||
* @param {string} provider - The provider of the acquirer
|
||||
* @param {number} acquirerId - The id of the acquirer handling the transaction
|
||||
* @param {string} code - The code of the provider
|
||||
* @param {number} providerId - The id of the provider handling the transaction
|
||||
* @param {object} processingValues - The processing values of the transaction
|
||||
* @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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @private
|
||||
* @param {string} provider - The provider of the acquirer
|
||||
* @param {number} acquirerId - The id of the acquirer handling the transaction
|
||||
* @param {string} code - The code of the provider
|
||||
* @param {number} providerId - The id of the provider handling the transaction
|
||||
* @param {object} processingValues - The processing values of the transaction
|
||||
* @return {undefined}
|
||||
*/
|
||||
_processRedirectPayment: (provider, acquirerId, processingValues) => {
|
||||
_processRedirectPayment: (code, providerId, processingValues) => {
|
||||
// Append the redirect form to the body
|
||||
const $redirectForm = $(processingValues.redirect_form_html).attr(
|
||||
'id', 'o_payment_redirect_form'
|
||||
@@ -404,16 +404,16 @@ odoo.define('payment.payment_form_mixin', require => {
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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 {object} processingValues - The processing values of the transaction
|
||||
* @return {undefined}
|
||||
*/
|
||||
_processTokenPayment: (provider, tokenId, processingValues) => {
|
||||
_processTokenPayment: (provider_code, tokenId, processingValues) => {
|
||||
// The flow is already completed as payments by tokens are immediately processed
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
.o_form_view {
|
||||
.o_payment_acquirer_desc {
|
||||
.o_payment_provider_desc {
|
||||
margin-top: 10px;
|
||||
ul {
|
||||
list-style-type: none;
|
||||
@@ -53,7 +53,7 @@
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="tx['display_message']">
|
||||
<!-- 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 t-else="">
|
||||
@@ -74,7 +74,7 @@
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="tx['display_message']">
|
||||
<!-- 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 t-else="">
|
||||
@@ -99,11 +99,11 @@
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="tx['display_message']">
|
||||
<!-- 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 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>
|
||||
</small>
|
||||
</a>
|
||||
|
||||
@@ -4,6 +4,6 @@ from . import common
|
||||
from . import http_common
|
||||
from . import test_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_transaction
|
||||
|
||||
@@ -59,7 +59,7 @@ class PaymentCommon(TransactionCase):
|
||||
'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 = """
|
||||
<form action="dummy" method="post">
|
||||
<input type="hidden" name="view_id" t-att-value="viewid"/>
|
||||
@@ -72,16 +72,16 @@ class PaymentCommon(TransactionCase):
|
||||
'arch': arch,
|
||||
})
|
||||
|
||||
cls.dummy_acquirer = cls.env['payment.acquirer'].create({
|
||||
'name': "Dummy Acquirer",
|
||||
'provider': 'none',
|
||||
cls.dummy_provider = cls.env['payment.provider'].create({
|
||||
'name': "Dummy Provider",
|
||||
'code': 'none',
|
||||
'state': 'test',
|
||||
'is_published': True,
|
||||
'allow_tokenization': True,
|
||||
'redirect_form_view_id': redirect_form.id,
|
||||
})
|
||||
|
||||
cls.acquirer = cls.dummy_acquirer
|
||||
cls.provider = cls.dummy_provider
|
||||
cls.amount = 1111.11
|
||||
cls.company = cls.env.company
|
||||
cls.company_id = cls.company.id
|
||||
@@ -102,7 +102,7 @@ class PaymentCommon(TransactionCase):
|
||||
super().setUp()
|
||||
if self.account_payment_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(
|
||||
'odoo.addons.account_payment.models.payment_transaction.PaymentTransaction._reconcile_after_done',
|
||||
)
|
||||
@@ -120,44 +120,44 @@ class PaymentCommon(TransactionCase):
|
||||
return currency
|
||||
|
||||
@classmethod
|
||||
def _prepare_acquirer(cls, provider='none', company=None, update_values=None):
|
||||
""" Prepare and return the first acquirer matching the given provider and company.
|
||||
def _prepare_provider(cls, code='none', company=None, update_values=None):
|
||||
""" 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 recordset company: The company of the acquirer to prepare, as a `res.company` record
|
||||
:param dict update_values: The values used to update the acquirer
|
||||
:return: The acquirer to prepare, if found
|
||||
:rtype: recordset of `payment.acquirer`
|
||||
:param str code: The code of the provider to prepare
|
||||
: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 provider
|
||||
:return: The provider to prepare, if found
|
||||
:rtype: recordset of `payment.provider`
|
||||
"""
|
||||
company = company or cls.env.company
|
||||
update_values = update_values or {}
|
||||
|
||||
acquirer = cls.env['payment.acquirer'].sudo().search(
|
||||
[('provider', '=', provider), ('company_id', '=', company.id)], limit=1
|
||||
provider = cls.env['payment.provider'].sudo().search(
|
||||
[('code', '=', code), ('company_id', '=', company.id)], limit=1
|
||||
)
|
||||
if not acquirer:
|
||||
base_acquirer = cls.env['payment.acquirer'].sudo().search(
|
||||
[('provider', '=', provider)], limit=1
|
||||
if not provider:
|
||||
base_provider = cls.env['payment.provider'].sudo().search(
|
||||
[('code', '=', code)], limit=1
|
||||
)
|
||||
if not base_acquirer:
|
||||
_logger.error("no payment.acquirer found for provider %s", provider)
|
||||
return cls.env['payment.acquirer']
|
||||
if not base_provider:
|
||||
_logger.error("no payment.provider found for code %s", code)
|
||||
return cls.env['payment.provider']
|
||||
else:
|
||||
acquirer = base_acquirer.copy({'company_id': company.id})
|
||||
provider = base_provider.copy({'company_id': company.id})
|
||||
|
||||
update_values['state'] = 'test'
|
||||
acquirer.write(update_values)
|
||||
return acquirer
|
||||
provider.write(update_values)
|
||||
return provider
|
||||
|
||||
def _create_transaction(self, flow, sudo=True, **values):
|
||||
default_values = {
|
||||
'amount': self.amount,
|
||||
'currency_id': self.currency.id,
|
||||
'acquirer_id': self.acquirer.id,
|
||||
'provider_id': self.provider.id,
|
||||
'reference': self.reference,
|
||||
'operation': f'online_{flow}',
|
||||
'partner_id': self.partner.id,
|
||||
@@ -167,9 +167,9 @@ class PaymentCommon(TransactionCase):
|
||||
def _create_token(self, sudo=True, **values):
|
||||
default_values = {
|
||||
'payment_details': "1234",
|
||||
'acquirer_id': self.acquirer.id,
|
||||
'provider_id': self.provider.id,
|
||||
'partner_id': self.partner.id,
|
||||
'acquirer_ref': "Acquirer Ref (TEST)",
|
||||
'provider_ref': "provider Ref (TEST)",
|
||||
'active': True,
|
||||
}
|
||||
return self.env['payment.token'].sudo(sudo).create(dict(default_values, **values))
|
||||
|
||||
@@ -85,12 +85,12 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
:param response: http Response, with a payment form as text
|
||||
: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
|
||||
"""
|
||||
# Need to specify an HTML parser as parser
|
||||
@@ -114,20 +114,20 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
|
||||
values[formatted_key] = formatted_val
|
||||
|
||||
payment_options_inputs = html_tree.xpath("//input[@name='o_payment_radio']")
|
||||
acquirer_ids = []
|
||||
provider_ids = []
|
||||
token_ids = []
|
||||
for p_o_input in payment_options_inputs:
|
||||
data = dict()
|
||||
for key, val in p_o_input.items():
|
||||
if key.startswith('data-'):
|
||||
data[key[5:]] = val
|
||||
if data['payment-option-type'] == 'acquirer':
|
||||
acquirer_ids.append(int(data['payment-option-id']))
|
||||
if data['payment-option-type'] == 'provider':
|
||||
provider_ids.append(int(data['payment-option-id']))
|
||||
else:
|
||||
token_ids.append(int(data['payment-option-id']))
|
||||
|
||||
values.update({
|
||||
'acquirer_ids': acquirer_ids,
|
||||
'provider_ids': provider_ids,
|
||||
'token_ids': token_ids,
|
||||
})
|
||||
|
||||
@@ -199,7 +199,7 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
|
||||
""" Prepare the basic payment/transaction route values.
|
||||
|
||||
: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
|
||||
:return: The route values
|
||||
:rtype: dict
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestFlows(PaymentHttpCommon):
|
||||
if key in route_values:
|
||||
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 = {
|
||||
@@ -47,7 +47,7 @@ class TestFlows(PaymentHttpCommon):
|
||||
}
|
||||
route_values.update({
|
||||
'flow': flow,
|
||||
'payment_option_id': self.acquirer.id,
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
})
|
||||
|
||||
@@ -59,14 +59,14 @@ class TestFlows(PaymentHttpCommon):
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
|
||||
# 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.currency_id.id, self.currency.id)
|
||||
self.assertEqual(tx_sudo.partner_id.id, self.partner.id)
|
||||
self.assertEqual(tx_sudo.reference, self.reference)
|
||||
|
||||
# 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['currency_id'], self.currency.id)
|
||||
self.assertEqual(processing_values['partner_id'], self.partner.id)
|
||||
@@ -89,7 +89,7 @@ class TestFlows(PaymentHttpCommon):
|
||||
str(self.user.id))
|
||||
self.assertEqual(
|
||||
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
|
||||
|
||||
@@ -160,8 +160,8 @@ class TestFlows(PaymentHttpCommon):
|
||||
# Fixed with freezegun
|
||||
expected_reference = 'V-20111102120021'
|
||||
|
||||
validation_amount = self.acquirer._get_validation_amount()
|
||||
validation_currency = self.acquirer._get_validation_currency()
|
||||
validation_amount = self.provider._get_validation_amount()
|
||||
validation_currency = self.provider._get_validation_currency()
|
||||
|
||||
tx_context = self._get_tx_manage_context()
|
||||
expected_values = {
|
||||
@@ -179,7 +179,7 @@ class TestFlows(PaymentHttpCommon):
|
||||
'partner_id': tx_context['partner_id'],
|
||||
'access_token': tx_context['access_token'],
|
||||
'flow': flow,
|
||||
'payment_option_id': self.acquirer.id,
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': True,
|
||||
'reference_prefix': tx_context['reference_prefix'],
|
||||
'landing_route': tx_context['landing_route'],
|
||||
@@ -190,13 +190,13 @@ class TestFlows(PaymentHttpCommon):
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
|
||||
# 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.currency_id.id, validation_currency.id)
|
||||
self.assertEqual(tx_sudo.partner_id.id, self.partner.id)
|
||||
self.assertEqual(tx_sudo.reference, expected_reference)
|
||||
# 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['currency_id'], validation_currency.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.update({
|
||||
'flow': 'this flow does not exist',
|
||||
'payment_option_id': self.acquirer.id,
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
'reference_prefix': 'whatever',
|
||||
'landing_route': 'whatever',
|
||||
@@ -304,37 +304,37 @@ class TestFlows(PaymentHttpCommon):
|
||||
"odoo.exceptions.ValidationError: The access token is invalid.",
|
||||
response.text)
|
||||
|
||||
def test_access_disabled_acquirers_tokens(self):
|
||||
def test_access_disabled_providers_tokens(self):
|
||||
self.partner = self.portal_partner
|
||||
|
||||
# Log in as user from Company A
|
||||
self.authenticate(self.portal_user.login, self.portal_user.login)
|
||||
|
||||
token = self._create_token()
|
||||
acquirer_b = self.acquirer.copy()
|
||||
acquirer_b.state = 'test'
|
||||
token_b = self._create_token(acquirer_id=acquirer_b.id)
|
||||
provider_b = self.provider.copy()
|
||||
provider_b.state = 'test'
|
||||
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()
|
||||
self.assertEqual(manage_context['partner_id'], self.partner.id)
|
||||
self.assertIn(self.acquirer.id, manage_context['acquirer_ids'])
|
||||
self.assertIn(acquirer_b.id, manage_context['acquirer_ids'])
|
||||
self.assertIn(self.provider.id, manage_context['provider_ids'])
|
||||
self.assertIn(provider_b.id, manage_context['provider_ids'])
|
||||
self.assertIn(token.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
|
||||
self.acquirer.state = 'disabled'
|
||||
# Token of disabled provider(s) & disabled providers should not be shown
|
||||
self.provider.state = 'disabled'
|
||||
manage_context = self._get_tx_manage_context()
|
||||
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])
|
||||
|
||||
# Archived tokens must be hidden from the user
|
||||
token_b.active = False
|
||||
manage_context = self._get_tx_manage_context()
|
||||
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'], [])
|
||||
|
||||
@mute_logger('odoo.addons.payment.models.payment_transaction')
|
||||
@@ -347,7 +347,7 @@ class TestFlows(PaymentHttpCommon):
|
||||
'._send_payment_request'
|
||||
) as patched:
|
||||
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)
|
||||
|
||||
@@ -361,7 +361,7 @@ class TestFlows(PaymentHttpCommon):
|
||||
'._send_payment_request'
|
||||
) as patched:
|
||||
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)
|
||||
|
||||
@@ -380,15 +380,15 @@ class TestFlows(PaymentHttpCommon):
|
||||
self.assertEqual(patched.call_count, 1)
|
||||
|
||||
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(
|
||||
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):
|
||||
self.acquirer.allow_tokenization = False
|
||||
self.provider.allow_tokenization = False
|
||||
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)],
|
||||
})
|
||||
|
||||
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):
|
||||
"""User pays for an amount in another company."""
|
||||
@@ -59,9 +59,9 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
|
||||
else:
|
||||
self.assertEqual(val, route_values[key])
|
||||
|
||||
available_acquirers = self.env['payment.acquirer'].sudo().browse(tx_context['acquirer_ids'])
|
||||
self.assertIn(self.acquirer_company_b, available_acquirers)
|
||||
self.assertEqual(available_acquirers.company_id, self.company_b)
|
||||
available_providers = self.env['payment.provider'].sudo().browse(tx_context['provider_ids'])
|
||||
self.assertIn(self.provider_company_b, available_providers)
|
||||
self.assertEqual(available_providers.company_id, self.company_b)
|
||||
|
||||
validation_values = {
|
||||
k: tx_context[k]
|
||||
@@ -76,7 +76,7 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
|
||||
}
|
||||
validation_values.update({
|
||||
'flow': 'direct',
|
||||
'payment_option_id': self.acquirer_company_b.id,
|
||||
'payment_option_id': self.provider_company_b.id,
|
||||
'tokenization_requested': False,
|
||||
})
|
||||
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 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.currency_id.id, self.currency.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.company_id, self.company_b)
|
||||
# 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['currency_id'], self.currency.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)
|
||||
|
||||
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,
|
||||
# even if they are in other companies otherwise he won't ever see them.
|
||||
manage_context = self._get_tx_manage_context()
|
||||
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_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):
|
||||
|
||||
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')
|
||||
for reference_index, operation in enumerate(
|
||||
('online_redirect', 'online_direct', 'online_token', 'validation', 'refund')
|
||||
@@ -29,7 +29,7 @@ class TestPaymentTransaction(PaymentCommon):
|
||||
)
|
||||
|
||||
def test_refund_transaction_values(self):
|
||||
self.acquirer.support_refund = 'partial'
|
||||
self.provider.support_refund = 'partial'
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
|
||||
# Test the default values of a full refund transaction
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<h1><field name="name" placeholder="Name"/></h1>
|
||||
</div>
|
||||
<notebook>
|
||||
<page string="Acquirers list" name="acquirers">
|
||||
<field nolabel="1" name="acquirer_ids"/>
|
||||
<page string="Providers list" name="providers">
|
||||
<field nolabel="1" name="provider_ids"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<template id="pay_meth_link" inherit_id="portal.portal_layout">
|
||||
<xpath expr="//div[hasclass('o_portal_my_details')]" position="inside">
|
||||
<t t-set="partner" t-value="request.env.user.partner_id"/>
|
||||
<t t-set="acquirers_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 t-set="providers_allowing_tokenization"
|
||||
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"/>
|
||||
<!-- 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'>
|
||||
<a href="/my/payment_method">Manage payment methods</a>
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<div t-elif="not partner_id" class="alert alert-warning">
|
||||
<strong>Warning</strong> You must be logged in to pay.
|
||||
</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/>
|
||||
If you believe that it is an error, please contact the website administrator.
|
||||
</div>
|
||||
@@ -76,9 +76,9 @@
|
||||
<!-- Manage page -->
|
||||
<div class="row">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +156,7 @@
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<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 class="col-md-4 offset-md-3 mt-2 ps-0">
|
||||
<a role="button"
|
||||
|
||||
+34
-34
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="payment_acquirer_form" model="ir.ui.view">
|
||||
<field name="name">payment.acquirer.form</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<record id="payment_provider_form" model="ir.ui.view">
|
||||
<field name="name">payment.provider.form</field>
|
||||
<field name="model">payment.provider</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Payment Acquirer">
|
||||
<form string="Payment provider">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="is_published" invisible="1"/>
|
||||
<field name="main_currency_id" invisible="1"/>
|
||||
@@ -25,7 +25,7 @@
|
||||
<field name="show_auth_msg" invisible="1"/>
|
||||
<field name="show_done_msg" invisible="1"/>
|
||||
<field name="show_cancel_msg" invisible="1"/>
|
||||
<field name="provider" invisible="1"/>
|
||||
<field name="code" invisible="1"/>
|
||||
<sheet>
|
||||
<!-- === Stat Buttons === -->
|
||||
<div class="oe_button_box" name="button_box">
|
||||
@@ -59,24 +59,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<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.
|
||||
</div>
|
||||
<group>
|
||||
<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="company_id" groups="base.group_multi_company" options='{"no_open":True}'/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook attrs="{'invisible': ['&', ('module_id', '!=', False), ('module_state', '!=', 'installed')]}">
|
||||
<page string="Credentials" name="acquirer_credentials" attrs="{'invisible': ['|', ('provider', '=', 'none'), ('show_credentials_page', '=', False)]}">
|
||||
<group name="acquirer"/>
|
||||
<page string="Credentials" name="provider_credentials" attrs="{'invisible': ['|', ('code', '=', 'none'), ('show_credentials_page', '=', False)]}">
|
||||
<group name="code"/>
|
||||
</page>
|
||||
<page string="Configuration" name="configuration">
|
||||
<group name="acquirer_config">
|
||||
<group name="provider_config">
|
||||
<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="allow_tokenization" attrs="{'invisible': ['|', ('support_tokenization', '=', False), ('show_allow_tokenization', '=', False)]}"/>
|
||||
<field name="capture_manually" attrs="{'invisible': [('support_manual_capture', '=', False)]}"/>
|
||||
@@ -118,14 +118,14 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_list" model="ir.ui.view">
|
||||
<field name="name">payment.acquirer.list</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<record id="payment_provider_list" model="ir.ui.view">
|
||||
<field name="name">payment.provider.list</field>
|
||||
<field name="model">payment.provider</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Payment Acquirers" create="false">
|
||||
<tree string="Payment Providers" create="false">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="provider"/>
|
||||
<field name="code"/>
|
||||
<field name="state"/>
|
||||
<field name="available_country_ids" widget="many2many_tags" optional="hide"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show"/>
|
||||
@@ -133,16 +133,16 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_kanban" model="ir.ui.view">
|
||||
<field name="name">payment.acquirer.kanban</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<record id="payment_provider_kanban" model="ir.ui.view">
|
||||
<field name="name">payment.provider.kanban</field>
|
||||
<field name="model">payment.provider</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban create="false" quick_create="false" class="o_kanban_dashboard">
|
||||
<field name="id"/>
|
||||
<field name="name"/>
|
||||
<field name="state"/>
|
||||
<field name="is_published"/>
|
||||
<field name="provider"/>
|
||||
<field name="code"/>
|
||||
<field name="module_id"/>
|
||||
<field name="module_state"/>
|
||||
<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="to_upgrade" t-value="!installed and to_buy"/>
|
||||
<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"
|
||||
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"
|
||||
alt="Acquirer"/>
|
||||
alt="provider"/>
|
||||
<div class="d-flex flex-column justify-content-between w-100">
|
||||
<div class="o_payment_kanban_info">
|
||||
<h4 class="mb-0"><t t-esc="record.name.value"/></h4>
|
||||
@@ -194,16 +194,16 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="payment_acquirer_search" model="ir.ui.view">
|
||||
<field name="name">payment.acquirer.search</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<record id="payment_provider_search" model="ir.ui.view">
|
||||
<field name="name">payment.provider.search</field>
|
||||
<field name="model">payment.provider</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" string="Acquirer" filter_domain="[('name', 'ilike', self)]"/>
|
||||
<field name="provider"/>
|
||||
<filter name="acquirer_installed" string="Installed" domain="[('module_state', '=', 'installed')]"/>
|
||||
<field name="name" string="provider" filter_domain="[('name', 'ilike', self)]"/>
|
||||
<field name="code"/>
|
||||
<filter name="provider_installed" string="Installed" domain="[('module_state', '=', 'installed')]"/>
|
||||
<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="Company" name="company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
@@ -211,13 +211,13 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_payment_acquirer" model="ir.actions.act_window">
|
||||
<field name="name">Payment Acquirers</field>
|
||||
<field name="res_model">payment.acquirer</field>
|
||||
<record id="action_payment_provider" model="ir.actions.act_window">
|
||||
<field name="name">Payment Providers</field>
|
||||
<field name="res_model">payment.provider</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new payment acquirer
|
||||
Create a new payment provider
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
@@ -4,10 +4,10 @@
|
||||
<!-- Checkout form -->
|
||||
<template id="checkout" name="Payment Checkout">
|
||||
<!-- Variables description:
|
||||
- 'acquirers' - The payment acquirers compatible with the current transaction
|
||||
- 'tokens' - The payment tokens of the current partner and payment acquirers
|
||||
- 'providers' - The payment providers compatible with the current transaction
|
||||
- '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
|
||||
- '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
|
||||
- 'reference_prefix' - The custom prefix to compute the full transaction reference
|
||||
- 'amount' - The amount to pay. Optional (sale_subscription)
|
||||
@@ -29,50 +29,50 @@
|
||||
t-att-data-landing-route="landing_route"
|
||||
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"/>
|
||||
<!-- 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 -->
|
||||
<t t-set="default_payment_option_id"
|
||||
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"/>
|
||||
<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-value="footer_template_id or 'payment.footer'"/>
|
||||
|
||||
<div class="card">
|
||||
<!-- === Acquirers === -->
|
||||
<t t-foreach="acquirers" t-as="acquirer">
|
||||
<!-- === Providers === -->
|
||||
<t t-foreach="providers" t-as="provider">
|
||||
<div name="o_payment_option_card" class="card-body o_payment_option_card">
|
||||
<label>
|
||||
<!-- === Radio button === -->
|
||||
<!-- Only shown if linked to the only payment option -->
|
||||
<input name="o_payment_radio"
|
||||
type="radio"
|
||||
t-att-checked="acquirer.id == default_payment_option_id"
|
||||
t-att-class="'' if acquirer_count + token_count > 1 else 'd-none'"
|
||||
t-att-data-payment-option-id="acquirer.id"
|
||||
t-att-data-provider="acquirer.provider"
|
||||
data-payment-option-type="acquirer"/>
|
||||
<!-- === Acquirer name === -->
|
||||
t-att-checked="provider.id == default_payment_option_id"
|
||||
t-att-class="'' if provider_count + token_count > 1 else 'd-none'"
|
||||
t-att-data-payment-option-id="provider.id"
|
||||
t-att-data-provider="provider.code"
|
||||
data-payment-option-type="provider"/>
|
||||
<!-- === Provider 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>
|
||||
<!-- === "Test Mode" badge === -->
|
||||
<span t-if="acquirer.state == 'test'"
|
||||
<span t-if="provider.state == 'test'"
|
||||
class="rounded-pill text-bg-warning ms-1">
|
||||
Test Mode
|
||||
</span>
|
||||
<!-- === "Unpublished" badge === -->
|
||||
<span t-if="not acquirer.is_published"
|
||||
<span t-if="not provider.is_published"
|
||||
class="badge text-bg-danger ms-1">
|
||||
Unpublished
|
||||
</span>
|
||||
<!-- === 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">
|
||||
+ <t t-esc="fees_by_acquirer.get(acquirer)"
|
||||
+ <t t-esc="fees_by_provider.get(provider)"
|
||||
t-options="{'widget': 'monetary', 'display_currency': currency}"/>
|
||||
Fees
|
||||
</span>
|
||||
@@ -81,26 +81,26 @@
|
||||
<!-- === Payment icon list === -->
|
||||
<t t-call="payment.icon_list"/>
|
||||
<!-- === Help message === -->
|
||||
<div t-if="not is_html_empty(acquirer.pre_msg)"
|
||||
t-out="acquirer.pre_msg"
|
||||
<div t-if="not is_html_empty(provider.pre_msg)"
|
||||
t-out="provider.pre_msg"
|
||||
class="text-muted ms-3"/>
|
||||
</div>
|
||||
<!-- === Acquirer inline form === -->
|
||||
<div t-attf-id="o_payment_acquirer_inline_form_{{acquirer.id}}"
|
||||
<!-- === Provider inline form === -->
|
||||
<div t-attf-id="o_payment_provider_inline_form_{{provider.id}}"
|
||||
name="o_payment_inline_form"
|
||||
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-value="acquirer.sudo().inline_form_view_id.xml_id"/>
|
||||
<!-- === Inline form content (filled by acquirer) === -->
|
||||
t-value="provider.sudo().inline_form_view_id.xml_id"/>
|
||||
<!-- === Inline form content (filled by provider) === -->
|
||||
<div t-if="inline_form_xml_id" class="clearfix">
|
||||
<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>
|
||||
</div>
|
||||
</t>
|
||||
<!-- === "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"/>
|
||||
Save my payment details
|
||||
</label>
|
||||
@@ -115,14 +115,14 @@
|
||||
type="radio"
|
||||
t-att-checked="token.id == default_payment_option_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"/>
|
||||
<!-- === Token name === -->
|
||||
<span class="payment_option_name" t-esc="token.display_name"/>
|
||||
<!-- === "V" check mark === -->
|
||||
<t t-call="payment.verified_token_checkmark"/>
|
||||
<!-- === "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
|
||||
</span>
|
||||
</label>
|
||||
@@ -132,8 +132,8 @@
|
||||
name="o_payment_inline_form"
|
||||
class="card-footer d-none">
|
||||
<t t-set="token_inline_form_xml_id"
|
||||
t-value="token.sudo().acquirer_id.token_inline_form_view_id.xml_id"/>
|
||||
<!-- === Inline form content (filled by acquirer) === -->
|
||||
t-value="token.sudo().provider_id.token_inline_form_view_id.xml_id"/>
|
||||
<!-- === Inline form content (filled by provider) === -->
|
||||
<div t-if="token_inline_form_xml_id" class="clearfix">
|
||||
<t t-call="{{token_inline_form_xml_id}}">
|
||||
<t t-set="token" t-value="token"/>
|
||||
@@ -153,7 +153,7 @@
|
||||
<!-- Manage (token create and deletion) form -->
|
||||
<template id="manage" name="Payment Manage">
|
||||
<!-- Variables description:
|
||||
- 'acquirers' - The payment acquirers supporting tokenization
|
||||
- 'providers' - The payment providers supporting tokenization
|
||||
- 'tokens' - The set of payment tokens of the current partner
|
||||
- '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
|
||||
@@ -176,62 +176,62 @@
|
||||
t-att-data-assign-token-route="assign_token_route"
|
||||
t-att-data-landing-route="landing_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="no_selectable_token" t-value="token_count == 0 or not assign_token_route"/>
|
||||
<t t-set="default_payment_option_id"
|
||||
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"/>
|
||||
<t t-set="footer_template_id"
|
||||
t-value="footer_template_id or 'payment.footer'"/>
|
||||
<div class="card">
|
||||
<!-- === Acquirers === -->
|
||||
<t t-foreach="acquirers" t-as="acquirer">
|
||||
<!-- === Providers === -->
|
||||
<t t-foreach="providers" t-as="provider">
|
||||
<div name="o_payment_option_card" class="card-body o_payment_option_card">
|
||||
<label>
|
||||
<!-- === Radio button === -->
|
||||
<!-- Only shown if linked to the only payment option -->
|
||||
<input name="o_payment_radio"
|
||||
type="radio"
|
||||
t-att-checked="acquirer.id == default_payment_option_id"
|
||||
t-att-class="'' if acquirer_count + token_count > 1 else 'd-none'"
|
||||
t-att-data-payment-option-id="acquirer.id"
|
||||
t-att-data-provider="acquirer.provider"
|
||||
data-payment-option-type="acquirer"/>
|
||||
<!-- === Acquirer name === -->
|
||||
t-att-checked="provider.id == default_payment_option_id"
|
||||
t-att-class="'' if provider_count + token_count > 1 else 'd-none'"
|
||||
t-att-data-payment-option-id="provider.id"
|
||||
t-att-data-provider="provider.code"
|
||||
data-payment-option-type="provider"/>
|
||||
<!-- === Provider 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>
|
||||
<!-- === "Test Mode" badge === -->
|
||||
<span t-if="acquirer.state == 'test'"
|
||||
<span t-if="provider.state == 'test'"
|
||||
class="rounded-pill text-bg-warning"
|
||||
style="margin-left:5px">
|
||||
Test Mode
|
||||
</span>
|
||||
<!-- === "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
|
||||
</span>
|
||||
</label>
|
||||
<!-- === Payment icon list === -->
|
||||
<t t-call="payment.icon_list"/>
|
||||
<!-- === Help message === -->
|
||||
<div t-if="not is_html_empty(acquirer.pre_msg)"
|
||||
t-out="acquirer.pre_msg"
|
||||
<div t-if="not is_html_empty(provider.pre_msg)"
|
||||
t-out="provider.pre_msg"
|
||||
class="text-muted ms-3"/>
|
||||
</div>
|
||||
<!-- === Acquirer inline form === -->
|
||||
<t t-if="acquirer.sudo()._should_build_inline_form(is_validation=True)">
|
||||
<div t-attf-id="o_payment_acquirer_inline_form_{{acquirer.id}}"
|
||||
<!-- === Provider inline form === -->
|
||||
<t t-if="provider.sudo()._should_build_inline_form(is_validation=True)">
|
||||
<div t-attf-id="o_payment_provider_inline_form_{{provider.id}}"
|
||||
name="o_payment_inline_form"
|
||||
class="card-footer d-none">
|
||||
<t t-set="inline_form_xml_id"
|
||||
t-value="acquirer.sudo().inline_form_view_id.xml_id"/>
|
||||
<!-- === Inline form content (filled by acquirer) === -->
|
||||
t-value="provider.sudo().inline_form_view_id.xml_id"/>
|
||||
<!-- === Inline form content (filled by provider) === -->
|
||||
<div t-if="inline_form_xml_id" class="clearfix">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,14 +248,14 @@
|
||||
t-att-checked="token.id == default_payment_option_id"
|
||||
t-att-class="'' if bool(assign_token_route) else 'd-none'"
|
||||
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"/>
|
||||
<!-- === Token name === -->
|
||||
<span class="payment_option_name" t-esc="token.display_name"/>
|
||||
<!-- === "V" check mark === -->
|
||||
<t t-call="payment.verified_token_checkmark"/>
|
||||
<!-- === "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">
|
||||
Unpublished
|
||||
</span>
|
||||
@@ -271,8 +271,8 @@
|
||||
name="o_payment_inline_form"
|
||||
class="card-footer d-none">
|
||||
<t t-set="token_inline_form_xml_id"
|
||||
t-value="token.sudo().acquirer_id.token_inline_form_view_id.xml_id"/>
|
||||
<!-- === Inline form content (filled by acquirer) === -->
|
||||
t-value="token.sudo().provider_id.token_inline_form_view_id.xml_id"/>
|
||||
<!-- === Inline form content (filled by provider) === -->
|
||||
<div t-if="token_inline_form_xml_id" class="clearfix">
|
||||
<t t-call="{{token_inline_form_xml_id}}">
|
||||
<t t-set="token" t-value="token"/>
|
||||
@@ -292,7 +292,7 @@
|
||||
<!-- Express Checkout form -->
|
||||
<template id="express_checkout" name="Payment Express Checkout">
|
||||
<!-- 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.
|
||||
- 'amount' - The amount to pay.
|
||||
- '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-express-route="express_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-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-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>
|
||||
@@ -339,7 +339,7 @@
|
||||
<t t-set="MAX_ICONS" t-value="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 < MAX_ICONS) else ' d-none'}}">
|
||||
<span t-esc="icon.image_payment_form"
|
||||
t-options="{'widget': 'image', 'alt-field': 'name'}"
|
||||
@@ -355,7 +355,7 @@
|
||||
<span class="float-end more_option text-info">
|
||||
<a name="o_payment_icon_more"
|
||||
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
|
||||
</a>
|
||||
</span>
|
||||
@@ -416,19 +416,19 @@
|
||||
</t>
|
||||
<t t-elif="tx.state == 'pending'">
|
||||
<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-elif="tx.state == 'authorized'">
|
||||
<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-elif="tx.state == 'done'">
|
||||
<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-elif="tx.state == 'cancel'">
|
||||
<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-elif="tx.state == 'error'">
|
||||
<t t-set="alert_style">danger</t>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
<field name="partner_id" />
|
||||
</group>
|
||||
<group name="technical_information">
|
||||
<field name="acquirer_id"/>
|
||||
<field name="acquirer_ref"/>
|
||||
<field name="provider_id"/>
|
||||
<field name="provider_ref"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</sheet>
|
||||
@@ -36,8 +36,8 @@
|
||||
<tree string="Payment Tokens">
|
||||
<field name="payment_details"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="acquirer_id" readonly="1"/>
|
||||
<field name="acquirer_ref" readonly="1"/>
|
||||
<field name="provider_id" readonly="1"/>
|
||||
<field name="provider_ref" readonly="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -52,7 +52,7 @@
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<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="Company" name="company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
<field name="amount"/>
|
||||
<field name="fees" attrs="{'invisible': [('fees', '=', 0.0)]}"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="acquirer_id"/>
|
||||
<field name="provider_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<!-- Used by some acquirer-specific views -->
|
||||
<field name="provider" invisible="1"/>
|
||||
<field name="acquirer_reference"/>
|
||||
<!-- Used by some provider-specific views -->
|
||||
<field name="provider_code" invisible="1"/>
|
||||
<field name="provider_reference"/>
|
||||
<field name="token_id" attrs="{'invisible': [('token_id', '=', False)]}"/>
|
||||
<field name="create_date"/>
|
||||
<field name="last_state_change"/>
|
||||
@@ -69,7 +69,7 @@
|
||||
<tree string="Payment Transactions" create="false">
|
||||
<field name="reference"/>
|
||||
<field name="create_date"/>
|
||||
<field name="acquirer_id"/>
|
||||
<field name="provider_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_name"/>
|
||||
<!-- Needed to display the currency of the amounts -->
|
||||
@@ -117,11 +117,11 @@
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="reference"/>
|
||||
<field name="acquirer_id"/>
|
||||
<field name="provider_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_name"/>
|
||||
<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="Status" name="state" context="{'group_by': 'state'}"/>
|
||||
<filter string="Company" name="company" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
|
||||
|
||||
@@ -35,22 +35,22 @@ class PaymentLinkWizard(models.TransientModel):
|
||||
description = fields.Char("Payment Ref")
|
||||
link = fields.Char(string="Payment Link", compute='_compute_link')
|
||||
company_id = fields.Many2one('res.company', compute='_compute_company_id')
|
||||
available_acquirer_ids = fields.Many2many(
|
||||
comodel_name='payment.acquirer',
|
||||
string="Payment Acquirers Available",
|
||||
compute='_compute_available_acquirer_ids',
|
||||
available_provider_ids = fields.Many2many(
|
||||
comodel_name='payment.provider',
|
||||
string="Payment Providers Available",
|
||||
compute='_compute_available_provider_ids',
|
||||
compute_sudo=True,
|
||||
)
|
||||
has_multiple_acquirers = fields.Boolean(
|
||||
string="Has Multiple Acquirers",
|
||||
compute='_compute_has_multiple_acquirers',
|
||||
has_multiple_providers = fields.Boolean(
|
||||
string="Has Multiple Providers",
|
||||
compute='_compute_has_multiple_providers',
|
||||
)
|
||||
payment_acquirer_selection = fields.Selection(
|
||||
string="Allow Payment Acquirer",
|
||||
help="If a specific payment acquirer is selected, customers will only be allowed to pay "
|
||||
payment_provider_selection = fields.Selection(
|
||||
string="Allow Payment Provider",
|
||||
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 "
|
||||
"acquirer.",
|
||||
selection='_selection_payment_acquirer_selection',
|
||||
"provider.",
|
||||
selection='_selection_payment_provider_selection',
|
||||
default='all',
|
||||
required=True,
|
||||
)
|
||||
@@ -69,9 +69,9 @@ class PaymentLinkWizard(models.TransientModel):
|
||||
link.company_id = record.company_id if 'company_id' in record else False
|
||||
|
||||
@api.depends('company_id', 'partner_id', 'currency_id')
|
||||
def _compute_available_acquirer_ids(self):
|
||||
def _compute_available_provider_ids(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_id=link.res_id,
|
||||
company_id=link.company_id.id,
|
||||
@@ -80,9 +80,9 @@ class PaymentLinkWizard(models.TransientModel):
|
||||
currency_id=link.currency_id.id,
|
||||
)
|
||||
|
||||
def _selection_payment_acquirer_selection(self):
|
||||
""" Specify available acquirers in the selection field.
|
||||
:return: The selection list of available acquirers.
|
||||
def _selection_payment_provider_selection(self):
|
||||
""" Specify available providers in the selection field.
|
||||
:return: The selection list of available providers.
|
||||
:rtype: list[tuple]
|
||||
"""
|
||||
defaults = self.default_get(['res_model', 'res_id'])
|
||||
@@ -96,7 +96,7 @@ class PaymentLinkWizard(models.TransientModel):
|
||||
partner_id = related_document.partner_id
|
||||
currency_id = related_document.currency_id
|
||||
selection.extend(
|
||||
self._get_payment_acquirer_available(
|
||||
self._get_payment_provider_available(
|
||||
res_model=res_model,
|
||||
res_id=res_id,
|
||||
company_id=company_id.id,
|
||||
@@ -107,18 +107,18 @@ class PaymentLinkWizard(models.TransientModel):
|
||||
)
|
||||
return selection
|
||||
|
||||
def _get_payment_acquirer_available(self, **kwargs):
|
||||
""" Select and return the acquirers matching the criteria.
|
||||
def _get_payment_provider_available(self, **kwargs):
|
||||
""" Select and return the providers matching the criteria.
|
||||
|
||||
:return: The compatible acquirers
|
||||
:rtype: recordset of `payment.acquirer`
|
||||
:return: The compatible providers
|
||||
: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')
|
||||
def _compute_has_multiple_acquirers(self):
|
||||
@api.depends('available_provider_ids')
|
||||
def _compute_has_multiple_providers(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):
|
||||
self.ensure_one()
|
||||
@@ -128,7 +128,7 @@ class PaymentLinkWizard(models.TransientModel):
|
||||
|
||||
@api.depends(
|
||||
'description', 'amount', 'currency_id', 'partner_id', 'company_id',
|
||||
'payment_acquirer_selection',
|
||||
'payment_provider_selection',
|
||||
)
|
||||
def _compute_link(self):
|
||||
for payment_link in self:
|
||||
@@ -140,8 +140,8 @@ class PaymentLinkWizard(models.TransientModel):
|
||||
'access_token': self._get_access_token(),
|
||||
**self._get_additional_link_values(),
|
||||
}
|
||||
if payment_link.payment_acquirer_selection != 'all':
|
||||
url_params['acquirer_id'] = str(payment_link.payment_acquirer_selection)
|
||||
if payment_link.payment_provider_selection != 'all':
|
||||
url_params['provider_id'] = str(payment_link.payment_provider_selection)
|
||||
payment_link.link = f'{base_url}/payment/pay?{urls.url_encode(url_params)}'
|
||||
|
||||
def _get_additional_link_values(self):
|
||||
|
||||
@@ -13,20 +13,20 @@
|
||||
<field name="partner_id" invisible="1"/>
|
||||
<field name="partner_email" invisible="1"/>
|
||||
<field name="amount_max" invisible="1"/>
|
||||
<field name="available_acquirer_ids" invisible="1"/>
|
||||
<field name="has_multiple_acquirers" invisible="1"/>
|
||||
<field name="available_provider_ids" invisible="1"/>
|
||||
<field name="has_multiple_providers" invisible="1"/>
|
||||
<field name="description"/>
|
||||
<field name="amount"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="payment_acquirer_selection"
|
||||
attrs="{'invisible':[('has_multiple_acquirers', '=', False)]}"/>
|
||||
<field name="payment_provider_selection"
|
||||
attrs="{'invisible':[('has_multiple_providers', '=', False)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="link" readonly="1" widget="CopyClipboardChar"/>
|
||||
</group>
|
||||
<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>
|
||||
<footer>
|
||||
<button string="Close" class="btn-primary" special="cancel" data-hotkey="z"/>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="payment_acquirer_onboarding_wizard_form" model="ir.ui.view">
|
||||
<field name="name">payment.acquirer.onboarding.wizard.form</field>
|
||||
<field name="model">payment.acquirer.onboarding.wizard</field>
|
||||
<record id="payment_provider_onboarding_wizard_form" model="ir.ui.view">
|
||||
<field name="name">payment.provider.onboarding.wizard.form</field>
|
||||
<field name="model">payment.provider.onboarding.wizard</field>
|
||||
<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="row align-items-start">
|
||||
<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>
|
||||
</p>
|
||||
<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>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -5,26 +5,26 @@ from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class PaymentWizard(models.TransientModel):
|
||||
_name = 'payment.acquirer.onboarding.wizard'
|
||||
_description = 'Payment acquire onboarding wizard'
|
||||
_name = 'payment.provider.onboarding.wizard'
|
||||
_description = 'Payment provider onboarding wizard'
|
||||
|
||||
payment_method = fields.Selection([
|
||||
('stripe', "Credit & Debit card (via Stripe)"),
|
||||
('paypal', "PayPal"),
|
||||
('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([
|
||||
('new_user', "I don't have a Paypal account"),
|
||||
('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_seller_account = fields.Char("Merchant Account ID", default=lambda self: self._get_default_payment_acquirer_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_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_provider_onboarding_value('paypal_seller_account'))
|
||||
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.
|
||||
manual_name = fields.Char("Method", default=lambda self: self._get_default_payment_acquirer_onboarding_value('manual_name'))
|
||||
journal_name = fields.Char("Bank Name", default=lambda self: self._get_default_payment_acquirer_onboarding_value('journal_name'))
|
||||
acc_number = fields.Char("Account Number", default=lambda self: self._get_default_payment_acquirer_onboarding_value('acc_number'))
|
||||
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_provider_onboarding_value('journal_name'))
|
||||
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")
|
||||
|
||||
_data_fetched = fields.Boolean(store=False)
|
||||
@@ -38,25 +38,25 @@ class PaymentWizard(models.TransientModel):
|
||||
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:
|
||||
env = self.env
|
||||
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)
|
||||
|
||||
def _get_default_payment_acquirer_onboarding_value(self, key):
|
||||
def _get_default_payment_provider_onboarding_value(self, key):
|
||||
if not self.env.is_admin():
|
||||
raise UserError(_("Only administrators can access this data."))
|
||||
|
||||
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._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([
|
||||
('name', 'in', ('payment_paypal', 'payment_stripe')),
|
||||
@@ -64,23 +64,23 @@ class PaymentWizard(models.TransientModel):
|
||||
]).mapped('name')
|
||||
|
||||
if 'payment_paypal' in installed_modules:
|
||||
acquirer = self.env.ref('payment.payment_acquirer_paypal')
|
||||
self._payment_acquirer_onboarding_cache['paypal_email_account'] = acquirer['paypal_email_account'] or self.env.user.email or ''
|
||||
self._payment_acquirer_onboarding_cache['paypal_seller_account'] = acquirer['paypal_seller_account']
|
||||
self._payment_acquirer_onboarding_cache['paypal_pdt_token'] = acquirer['paypal_pdt_token']
|
||||
provider = self.env.ref('payment.payment_provider_paypal')
|
||||
self._payment_provider_onboarding_cache['paypal_email_account'] = provider['paypal_email_account'] or self.env.user.email or ''
|
||||
self._payment_provider_onboarding_cache['paypal_seller_account'] = provider['paypal_seller_account']
|
||||
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
|
||||
|
||||
self._payment_acquirer_onboarding_cache['manual_name'] = manual_payment['name']
|
||||
self._payment_acquirer_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_acquirer_onboarding_cache['acc_number'] = journal.bank_acc_number
|
||||
self._payment_provider_onboarding_cache['manual_name'] = manual_payment['name']
|
||||
self._payment_provider_onboarding_cache['manual_post_msg'] = manual_payment['pending_msg']
|
||||
self._payment_provider_onboarding_cache['journal_name'] = journal.name if journal.name != "Bank" else ""
|
||||
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):
|
||||
""" Install required payment acquirers, configure them and mark the
|
||||
""" Install required payment providers, configure them and mark the
|
||||
onboarding step as done."""
|
||||
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)
|
||||
|
||||
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_seller_account': self.paypal_seller_account,
|
||||
'paypal_pdt_token': self.paypal_pdt_token,
|
||||
'state': 'enabled',
|
||||
})
|
||||
elif self.payment_method == 'manual':
|
||||
manual_acquirer = self._get_manual_payment_acquirer(new_env)
|
||||
if not manual_acquirer:
|
||||
manual_provider = self._get_manual_payment_provider(new_env)
|
||||
if not manual_provider:
|
||||
raise UserError(_(
|
||||
'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_acquirer.pending_msg = self.manual_post_msg
|
||||
manual_acquirer.state = 'enabled'
|
||||
manual_provider.name = self.manual_name
|
||||
manual_provider.pending_msg = self.manual_post_msg
|
||||
manual_provider.state = 'enabled'
|
||||
|
||||
journal = manual_acquirer.journal_id
|
||||
journal = manual_provider.journal_id
|
||||
if journal:
|
||||
journal.name = self.journal_name
|
||||
journal.bank_acc_number = self.acc_number
|
||||
@@ -126,14 +126,14 @@ class PaymentWizard(models.TransientModel):
|
||||
return self._start_stripe_onboarding()
|
||||
|
||||
# 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'}
|
||||
|
||||
def _set_payment_acquirer_onboarding_step_done(self):
|
||||
self.env.company.sudo().set_onboarding_step_done('payment_acquirer_onboarding_state')
|
||||
def _set_payment_provider_onboarding_step_done(self):
|
||||
self.env.company.sudo().set_onboarding_step_done('payment_provider_onboarding_state')
|
||||
|
||||
def _start_stripe_onboarding(self):
|
||||
""" 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.
|
||||
return self.env.company._run_payment_onboarding_step(menu_id)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from . import controllers
|
||||
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):
|
||||
@@ -11,4 +11,4 @@ def post_init_hook(cr, registry):
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
reset_payment_acquirer(cr, registry, 'adyen')
|
||||
reset_payment_provider(cr, registry, 'adyen')
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Adyen Payment Acquirer',
|
||||
'name': 'Adyen Payment Provider',
|
||||
'version': '2.0',
|
||||
'category': 'Accounting/Payment Acquirers',
|
||||
'category': 'Accounting/Payment Providers',
|
||||
'sequence': 340,
|
||||
'summary': 'Payment Acquirer: Adyen Implementation',
|
||||
'description': """Adyen Payment Acquirer""",
|
||||
'summary': 'Payment Provider: Adyen Implementation',
|
||||
'description': """Adyen Payment Provider""",
|
||||
'depends': ['payment'],
|
||||
'data': [
|
||||
'views/payment_adyen_templates.xml',
|
||||
'views/payment_views.xml',
|
||||
'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,
|
||||
'post_init_hook': 'post_init_hook',
|
||||
|
||||
@@ -24,32 +24,32 @@ class AdyenController(http.Controller):
|
||||
|
||||
_webhook_url = '/payment/adyen/notification'
|
||||
|
||||
@http.route('/payment/adyen/acquirer_info', type='json', auth='public')
|
||||
def adyen_acquirer_info(self, acquirer_id):
|
||||
""" Return public information on the acquirer.
|
||||
@http.route('/payment/adyen/provider_info', type='json', auth='public')
|
||||
def adyen_provider_info(self, provider_id):
|
||||
""" Return public information on the provider.
|
||||
|
||||
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id
|
||||
:return: Public information on the acquirer, namely: the state and client key
|
||||
:param int provider_id: The provider handling the transaction, as a `payment.provider` id
|
||||
:return: Public information on the provider, namely: the state and client key
|
||||
: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 {
|
||||
'state': acquirer_sudo.state,
|
||||
'client_key': acquirer_sudo.adyen_client_key,
|
||||
'state': provider_sudo.state,
|
||||
'client_key': provider_sudo.adyen_client_key,
|
||||
}
|
||||
|
||||
@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.
|
||||
|
||||
: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 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
|
||||
:return: The JSON-formatted content of the response
|
||||
: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_code = currency_id and currency.name
|
||||
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('_', '-')
|
||||
shopper_reference = partner_sudo and f'ODOO_PARTNER_{partner_sudo.id}'
|
||||
data = {
|
||||
'merchantAccount': acquirer_sudo.adyen_merchant_account,
|
||||
'merchantAccount': provider_sudo.adyen_merchant_account,
|
||||
'amount': converted_amount,
|
||||
'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')
|
||||
'shopperReference': shopper_reference,
|
||||
'channel': 'Web',
|
||||
}
|
||||
response_content = acquirer_sudo._adyen_make_request(
|
||||
response_content = provider_sudo._adyen_make_request(
|
||||
url_field_name='adyen_checkout_api_url',
|
||||
endpoint='/paymentMethods',
|
||||
payload=data,
|
||||
@@ -82,12 +82,12 @@ class AdyenController(http.Controller):
|
||||
|
||||
@http.route('/payment/adyen/payments', type='json', auth='public')
|
||||
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
|
||||
):
|
||||
""" 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 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
|
||||
@@ -106,17 +106,17 @@ class AdyenController(http.Controller):
|
||||
raise ValidationError("Adyen: " + _("Received tampered payment request data."))
|
||||
|
||||
# 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)])
|
||||
data = {
|
||||
'merchantAccount': acquirer_sudo.adyen_merchant_account,
|
||||
'merchantAccount': provider_sudo.adyen_merchant_account,
|
||||
'amount': {
|
||||
'value': converted_amount,
|
||||
'currency': request.env['res.currency'].browse(currency_id).name, # ISO 4217
|
||||
},
|
||||
'reference': reference,
|
||||
'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
|
||||
'shopperIP': payment_utils.get_customer_ip_address(),
|
||||
'shopperInteraction': 'Ecommerce',
|
||||
@@ -125,10 +125,10 @@ class AdyenController(http.Controller):
|
||||
'allow3DS2': True
|
||||
},
|
||||
'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
|
||||
'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.
|
||||
# The key 'merchantReference' is chosen on purpose to be the same as that returned
|
||||
# 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
|
||||
# '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
|
||||
# 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
|
||||
# captured on Odoo.
|
||||
if not acquirer_sudo.capture_manually:
|
||||
if not provider_sudo.capture_manually:
|
||||
data.update(captureDelayHours=0)
|
||||
|
||||
# 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',
|
||||
endpoint='/payments',
|
||||
payload=data,
|
||||
@@ -165,21 +165,21 @@ class AdyenController(http.Controller):
|
||||
return response_content
|
||||
|
||||
@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.
|
||||
|
||||
The additional actions can have been performed both from the inline form or during a
|
||||
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 dict payment_details: The details of the additional actions performed for the payment
|
||||
:return: The JSON-formatted content of the response
|
||||
:rtype: dict
|
||||
"""
|
||||
# Make the payment details request to Adyen
|
||||
acquirer_sudo = request.env['payment.acquirer'].browse(acquirer_id).sudo()
|
||||
response_content = acquirer_sudo._adyen_make_request(
|
||||
provider_sudo = request.env['payment.provider'].browse(provider_id).sudo()
|
||||
response_content = provider_sudo._adyen_make_request(
|
||||
url_field_name='adyen_checkout_api_url',
|
||||
endpoint='/payments/details',
|
||||
payload=payment_details,
|
||||
@@ -229,7 +229,7 @@ class AdyenController(http.Controller):
|
||||
tx_sudo.reference, pprint.pformat(data)
|
||||
)
|
||||
self.adyen_payment_details(
|
||||
tx_sudo.acquirer_id.id,
|
||||
tx_sudo.provider_id.id,
|
||||
data['merchantReference'],
|
||||
{
|
||||
'details': {
|
||||
@@ -302,7 +302,7 @@ class AdyenController(http.Controller):
|
||||
raise Forbidden()
|
||||
|
||||
# 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)
|
||||
if not hmac.compare_digest(received_signature, expected_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
|
||||
|
||||
: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
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="payment.payment_acquirer_adyen" model="payment.acquirer">
|
||||
<field name="provider">adyen</field>
|
||||
<record id="payment.payment_provider_adyen" model="payment.provider">
|
||||
<field name="code">adyen</field>
|
||||
<field name="inline_form_view_id" ref="inline_form"/>
|
||||
<field name="allow_tokenization">True</field>
|
||||
</record>
|
||||
@@ -1,5 +1,5 @@
|
||||
# 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_transaction
|
||||
|
||||
+7
-7
@@ -13,14 +13,14 @@ from odoo.addons.payment_adyen.const import API_ENDPOINT_VERSIONS
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentAcquirer(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
class PaymentProvider(models.Model):
|
||||
_inherit = 'payment.provider'
|
||||
|
||||
provider = fields.Selection(
|
||||
code = fields.Selection(
|
||||
selection_add=[('adyen', "Adyen")], ondelete={'adyen': 'set default'})
|
||||
adyen_merchant_account = fields.Char(
|
||||
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')
|
||||
adyen_api_key = fields.Char(
|
||||
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
|
||||
"""
|
||||
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])
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
@@ -66,7 +66,7 @@ class PaymentAcquirer(models.Model):
|
||||
def _compute_feature_support_fields(self):
|
||||
""" Override of `payment` to enable additional features. """
|
||||
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_refund': 'partial',
|
||||
'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 endpoint: The endpoint to be reached by the request
|
||||
: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.
|
||||
:param dict payload: The payload 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`
|
||||
|
||||
: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
|
||||
"""
|
||||
res = super()._get_specific_processing_values(processing_values)
|
||||
if self.provider != 'adyen':
|
||||
if self.provider_code != 'adyen':
|
||||
return res
|
||||
|
||||
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
|
||||
"""
|
||||
super()._send_payment_request()
|
||||
if self.provider != 'adyen':
|
||||
if self.provider_code != 'adyen':
|
||||
return
|
||||
|
||||
# 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)
|
||||
)
|
||||
data = {
|
||||
'merchantAccount': self.acquirer_id.adyen_merchant_account,
|
||||
'merchantAccount': self.provider_id.adyen_merchant_account,
|
||||
'amount': {
|
||||
'value': converted_amount,
|
||||
'currency': self.currency_id.name,
|
||||
},
|
||||
'reference': self.reference,
|
||||
'paymentMethod': {
|
||||
'recurringDetailReference': self.token_id.acquirer_ref,
|
||||
'recurringDetailReference': self.token_id.provider_ref,
|
||||
},
|
||||
'shopperReference': self.token_id.adyen_shopper_reference,
|
||||
'recurringProcessingModel': 'Subscription',
|
||||
@@ -77,18 +77,18 @@ class PaymentTransaction(models.Model):
|
||||
'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
|
||||
# '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
|
||||
# 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
|
||||
# captured on Odoo.
|
||||
if not self.acquirer_id.capture_manually:
|
||||
if not self.provider_id.capture_manually:
|
||||
data.update(captureDelayHours=0)
|
||||
|
||||
# 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',
|
||||
endpoint='/payments',
|
||||
payload=data,
|
||||
@@ -112,7 +112,7 @@ class PaymentTransaction(models.Model):
|
||||
:return: The refund transaction if any
|
||||
:rtype: recordset of `payment.transaction`
|
||||
"""
|
||||
if self.provider != 'adyen':
|
||||
if self.provider_code != 'adyen':
|
||||
return super()._send_refund_request(
|
||||
amount_to_refund=amount_to_refund,
|
||||
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)
|
||||
)
|
||||
data = {
|
||||
'merchantAccount': self.acquirer_id.adyen_merchant_account,
|
||||
'merchantAccount': self.provider_id.adyen_merchant_account,
|
||||
'amount': {
|
||||
'value': converted_amount,
|
||||
'currency': refund_tx.currency_id.name,
|
||||
},
|
||||
'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',
|
||||
endpoint='/payments/{}/refunds',
|
||||
endpoint_param=self.acquirer_reference,
|
||||
endpoint_param=self.provider_reference,
|
||||
payload=data,
|
||||
method='POST'
|
||||
)
|
||||
@@ -153,7 +153,7 @@ class PaymentTransaction(models.Model):
|
||||
if psp_reference and status == 'received':
|
||||
# The PSP reference associated with this /refunds request is different from the psp
|
||||
# reference associated with the original payment request.
|
||||
refund_tx.acquirer_reference = psp_reference
|
||||
refund_tx.provider_reference = psp_reference
|
||||
|
||||
return refund_tx
|
||||
|
||||
@@ -165,24 +165,24 @@ class PaymentTransaction(models.Model):
|
||||
:return: None
|
||||
"""
|
||||
super()._send_capture_request()
|
||||
if self.provider != 'adyen':
|
||||
if self.provider_code != 'adyen':
|
||||
return
|
||||
|
||||
converted_amount = payment_utils.to_minor_currency_units(
|
||||
self.amount, self.currency_id, CURRENCY_DECIMALS.get(self.currency_id.name)
|
||||
)
|
||||
data = {
|
||||
'merchantAccount': self.acquirer_id.adyen_merchant_account,
|
||||
'merchantAccount': self.provider_id.adyen_merchant_account,
|
||||
'amount': {
|
||||
'value': converted_amount,
|
||||
'currency': self.currency_id.name,
|
||||
},
|
||||
'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',
|
||||
endpoint='/payments/{}/captures',
|
||||
endpoint_param=self.acquirer_reference,
|
||||
endpoint_param=self.provider_reference,
|
||||
payload=data,
|
||||
method='POST',
|
||||
)
|
||||
@@ -193,7 +193,7 @@ class PaymentTransaction(models.Model):
|
||||
if status == 'received':
|
||||
self._log_message_on_linked_documents(_(
|
||||
"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):
|
||||
@@ -204,17 +204,17 @@ class PaymentTransaction(models.Model):
|
||||
:return: None
|
||||
"""
|
||||
super()._send_void_request()
|
||||
if self.provider != 'adyen':
|
||||
if self.provider_code != 'adyen':
|
||||
return
|
||||
|
||||
data = {
|
||||
'merchantAccount': self.acquirer_id.adyen_merchant_account,
|
||||
'merchantAccount': self.provider_id.adyen_merchant_account,
|
||||
'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',
|
||||
endpoint='/payments/{}/cancels',
|
||||
endpoint_param=self.acquirer_reference,
|
||||
endpoint_param=self.provider_reference,
|
||||
payload=data,
|
||||
method='POST',
|
||||
)
|
||||
@@ -225,21 +225,21 @@ class PaymentTransaction(models.Model):
|
||||
if status == 'received':
|
||||
self._log_message_on_linked_documents(_(
|
||||
"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.
|
||||
|
||||
: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
|
||||
:return: The transaction if found
|
||||
:rtype: recordset of `payment.transaction`
|
||||
:raise: ValidationError if inconsistent data were received
|
||||
:raise: ValidationError if the data match no transaction
|
||||
"""
|
||||
tx = super()._get_tx_from_notification_data(provider, notification_data)
|
||||
if provider != 'adyen' or len(tx) == 1:
|
||||
tx = super()._get_tx_from_notification_data(provider_code, notification_data)
|
||||
if provider_code != 'adyen' or len(tx) == 1:
|
||||
return tx
|
||||
|
||||
reference = notification_data.get('merchantReference')
|
||||
@@ -247,28 +247,28 @@ class PaymentTransaction(models.Model):
|
||||
raise ValidationError("Adyen: " + _("Received data with missing merchant reference"))
|
||||
|
||||
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')
|
||||
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']:
|
||||
# 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.
|
||||
tx = self.search(
|
||||
[('acquirer_reference', '=', source_reference), ('provider', '=', 'adyen')]
|
||||
[('provider_reference', '=', source_reference), ('provider_code', '=', 'adyen')]
|
||||
)
|
||||
else: # 'REFUND'
|
||||
# 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
|
||||
# acquirer reference.
|
||||
# provider reference.
|
||||
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
|
||||
# Find the source transaction based on the original reference
|
||||
source_tx = self.search(
|
||||
[('acquirer_reference', '=', source_reference), ('provider', '=', 'adyen')]
|
||||
[('provider_reference', '=', source_reference), ('provider_code', '=', 'adyen')]
|
||||
)
|
||||
if source_tx:
|
||||
# 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`
|
||||
: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')
|
||||
if not refund_acquirer_reference or not amount_to_refund:
|
||||
if not refund_provider_reference or not amount_to_refund:
|
||||
raise ValidationError(
|
||||
"Adyen: " + _("Received refund data with missing transaction values")
|
||||
)
|
||||
@@ -307,7 +307,7 @@ class PaymentTransaction(models.Model):
|
||||
amount_to_refund, source_tx.currency_id
|
||||
)
|
||||
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):
|
||||
@@ -320,7 +320,7 @@ class PaymentTransaction(models.Model):
|
||||
:raise: ValidationError if inconsistent data were received
|
||||
"""
|
||||
super()._process_notification_data(notification_data)
|
||||
if self.provider != 'adyen':
|
||||
if self.provider_code != 'adyen':
|
||||
return
|
||||
|
||||
# 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.
|
||||
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.
|
||||
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
|
||||
payment_state = notification_data.get('resultCode')
|
||||
@@ -347,7 +347,7 @@ class PaymentTransaction(models.Model):
|
||||
if self.tokenize and has_token_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()
|
||||
else: # The payment was configured for manual capture.
|
||||
# Differentiate the state based on the event code.
|
||||
@@ -414,10 +414,10 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
additional_data = notification_data['additionalData']
|
||||
token = self.env['payment.token'].create({
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'provider_id': self.provider_id.id,
|
||||
'payment_details': additional_data.get('cardSummary'),
|
||||
'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'],
|
||||
'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({
|
||||
route: '/payment/adyen/payment_details',
|
||||
params: {
|
||||
'acquirer_id': dropin.acquirerId,
|
||||
'provider_id': dropin.providerId,
|
||||
'reference': this.adyenDropin.reference,
|
||||
'payment_details': state.data,
|
||||
},
|
||||
@@ -72,14 +72,14 @@ odoo.define('payment_adyen.payment_form', require => {
|
||||
// Create the transaction and retrieve the processing values
|
||||
return this._rpc({
|
||||
route: this.txContext.transactionRoute,
|
||||
params: this._prepareTransactionRouteParams('adyen', dropin.acquirerId, 'direct'),
|
||||
params: this._prepareTransactionRouteParams('adyen', dropin.providerId, 'direct'),
|
||||
}).then(processingValues => {
|
||||
this.adyenDropin.reference = processingValues.reference; // Store final reference
|
||||
// Initiate the payment
|
||||
return this._rpc({
|
||||
route: '/payment/adyen/payments',
|
||||
params: {
|
||||
'acquirer_id': dropin.acquirerId,
|
||||
'provider_id': dropin.providerId,
|
||||
'reference': processingValues.reference,
|
||||
'converted_amount': processingValues.converted_amount,
|
||||
'currency_id': processingValues.currency_id,
|
||||
@@ -112,39 +112,39 @@ odoo.define('payment_adyen.payment_form', require => {
|
||||
*
|
||||
* @override method from payment.payment_form_mixin
|
||||
* @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 {string} flow - The online payment flow of the selected payment option
|
||||
* @return {Promise}
|
||||
*/
|
||||
_prepareInlineForm: function (provider, paymentOptionId, flow) {
|
||||
if (provider !== 'adyen') {
|
||||
_prepareInlineForm: function (code, paymentOptionId, flow) {
|
||||
if (code !== 'adyen') {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
|
||||
// Check if instantiation of the drop-in is needed
|
||||
if (flow === 'token') {
|
||||
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
|
||||
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
|
||||
this._setPaymentFlow('direct');
|
||||
|
||||
// Get public information on the acquirer (state, client_key)
|
||||
// Get public information on the provider (state, client_key)
|
||||
return this._rpc({
|
||||
route: '/payment/adyen/acquirer_info',
|
||||
route: '/payment/adyen/provider_info',
|
||||
params: {
|
||||
'acquirer_id': paymentOptionId,
|
||||
'provider_id': paymentOptionId,
|
||||
},
|
||||
}).then(acquirerInfo => {
|
||||
}).then(providerInfo => {
|
||||
// Get the available payment methods
|
||||
return this._rpc({
|
||||
route: '/payment/adyen/payment_methods',
|
||||
params: {
|
||||
'acquirer_id': paymentOptionId,
|
||||
'provider_id': paymentOptionId,
|
||||
'partner_id': parseInt(this.txContext.partnerId),
|
||||
'amount': this.txContext.amount
|
||||
? parseFloat(this.txContext.amount)
|
||||
@@ -157,9 +157,9 @@ odoo.define('payment_adyen.payment_form', require => {
|
||||
// Instantiate the drop-in
|
||||
const configuration = {
|
||||
paymentMethodsResponse: paymentMethodsResult,
|
||||
clientKey: acquirerInfo.client_key,
|
||||
clientKey: providerInfo.client_key,
|
||||
locale: (this._getContext().lang || 'en-US').replace('_', '-'),
|
||||
environment: acquirerInfo.state === 'enabled' ? 'live' : 'test',
|
||||
environment: providerInfo.state === 'enabled' ? 'live' : 'test',
|
||||
onAdditionalDetails: this._dropinOnAdditionalDetails.bind(this),
|
||||
onError: this._dropinOnError.bind(this),
|
||||
onSubmit: this._dropinOnSubmit.bind(this),
|
||||
@@ -175,7 +175,7 @@ odoo.define('payment_adyen.payment_form', require => {
|
||||
setStatusAutomatically: true,
|
||||
},
|
||||
).mount(`#o_adyen_dropin_container_${paymentOptionId}`);
|
||||
this.adyenDropin.acquirerId = paymentOptionId;
|
||||
this.adyenDropin.providerId = paymentOptionId;
|
||||
});
|
||||
}).guardedCatch((error) => {
|
||||
error.event.preventDefault();
|
||||
@@ -192,7 +192,7 @@ odoo.define('payment_adyen.payment_form', require => {
|
||||
*
|
||||
* @override method from payment.payment_form_mixin
|
||||
* @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 {string} flow - The online payment flow of the transaction
|
||||
* @return {Promise}
|
||||
|
||||
@@ -9,7 +9,7 @@ class AdyenCommon(PaymentCommon):
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.adyen = cls._prepare_acquirer('adyen', update_values={
|
||||
cls.adyen = cls._prepare_provider('adyen', update_values={
|
||||
'adyen_merchant_account': 'dummy',
|
||||
'adyen_api_key': 'dummy',
|
||||
'adyen_client_key': 'dummy',
|
||||
@@ -19,7 +19,7 @@ class AdyenCommon(PaymentCommon):
|
||||
})
|
||||
|
||||
# Override default values
|
||||
cls.acquirer = cls.adyen
|
||||
cls.provider = cls.adyen
|
||||
|
||||
cls.psp_reference = '0123456789ABCDEF'
|
||||
cls.original_reference = 'FEDCBA9876543210'
|
||||
@@ -46,7 +46,7 @@ class AdyenCommon(PaymentCommon):
|
||||
]
|
||||
}
|
||||
|
||||
def _create_transaction(self, *args, acquirer_reference=None, **kwargs):
|
||||
if not acquirer_reference:
|
||||
acquirer_reference = self.psp_reference
|
||||
return super()._create_transaction(*args, acquirer_reference=acquirer_reference, **kwargs)
|
||||
def _create_transaction(self, *args, provider_reference=None, **kwargs):
|
||||
if not provider_reference:
|
||||
provider_reference = self.psp_reference
|
||||
return super()._create_transaction(*args, provider_reference=provider_reference, **kwargs)
|
||||
|
||||
@@ -41,15 +41,15 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
|
||||
@mute_logger('odoo.addons.payment_adyen.models.payment_transaction')
|
||||
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(
|
||||
'redirect', state='done', acquirer_reference='source_reference'
|
||||
'redirect', state='done', provider_reference='source_reference'
|
||||
)
|
||||
tx._reconcile_after_done() # Create the payment
|
||||
|
||||
# Send the refund request
|
||||
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"}
|
||||
):
|
||||
tx._send_refund_request()
|
||||
@@ -65,20 +65,20 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
"until a success notification is sent"
|
||||
)
|
||||
self.assertNotEqual(
|
||||
refund_tx.acquirer_reference,
|
||||
tx.acquirer_reference,
|
||||
msg="The acquirer reference of the refund transaction should be different from that of "
|
||||
refund_tx.provider_reference,
|
||||
tx.provider_reference,
|
||||
msg="The provider reference of the refund transaction should be different from that of "
|
||||
"the source transaction."
|
||||
)
|
||||
|
||||
def test_get_tx_from_notification_data_returns_refund_tx(self):
|
||||
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(
|
||||
'direct',
|
||||
reference='RefundTx',
|
||||
acquirer_reference=self.psp_reference,
|
||||
provider_reference=self.psp_reference,
|
||||
amount=-source_tx.amount,
|
||||
operation='refund',
|
||||
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):
|
||||
source_tx = self._create_transaction(
|
||||
'direct', state='done', acquirer_reference=self.original_reference
|
||||
'direct', state='done', provider_reference=self.original_reference
|
||||
)
|
||||
data = dict(
|
||||
self.webhook_notification_payload,
|
||||
@@ -117,11 +117,11 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
|
||||
@mute_logger('odoo.addons.payment_adyen.models.payment_transaction')
|
||||
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')
|
||||
|
||||
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'},
|
||||
):
|
||||
tx._send_capture_request()
|
||||
@@ -134,11 +134,11 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
|
||||
@mute_logger('odoo.addons.payment_adyen.models.payment_transaction')
|
||||
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')
|
||||
|
||||
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'},
|
||||
):
|
||||
tx._send_void_request()
|
||||
@@ -155,7 +155,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
self.assertEqual(tx.state, 'done')
|
||||
|
||||
def test_webhook_notification_authorizes_transaction(self):
|
||||
self.acquirer.capture_manually = True
|
||||
self.provider.capture_manually = True
|
||||
tx = self._create_transaction('direct')
|
||||
self._webhook_notification_flow(self.webhook_notification_batch_data)
|
||||
self.assertEqual(
|
||||
@@ -166,9 +166,9 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
)
|
||||
|
||||
def test_webhook_notification_captures_transaction(self):
|
||||
self.acquirer.capture_manually = True
|
||||
self.provider.capture_manually = True
|
||||
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=[{
|
||||
'NotificationRequestItem': dict(self.webhook_notification_payload, eventCode='CAPTURE')
|
||||
@@ -181,7 +181,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
|
||||
def test_webhook_notification_cancels_transaction(self):
|
||||
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=[{
|
||||
'NotificationRequestItem': dict(
|
||||
@@ -197,7 +197,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
|
||||
def test_webhook_notification_refunds_transaction(self):
|
||||
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=[{
|
||||
'NotificationRequestItem': dict(
|
||||
@@ -232,7 +232,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
|
||||
def test_failed_webhook_capture_notification_leaves_transaction_authorized(self):
|
||||
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=[{
|
||||
'NotificationRequestItem': dict(
|
||||
@@ -260,7 +260,7 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
|
||||
def test_failed_webhook_refund_notification_sets_refund_transaction_in_error(self):
|
||||
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=[{
|
||||
'NotificationRequestItem': dict(
|
||||
@@ -336,8 +336,8 @@ class AdyenTest(AdyenCommon, PaymentHttpCommon):
|
||||
self.assertRaises(Forbidden, AdyenController._verify_notification_signature, payload, tx)
|
||||
|
||||
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.acquirer.adyen_api_key, False)
|
||||
self.assertEqual(self.acquirer.adyen_hmac_key, False)
|
||||
self.assertEqual(self.provider.adyen_merchant_account, False)
|
||||
self.assertEqual(self.provider.adyen_api_key, False)
|
||||
self.assertEqual(self.provider.adyen_hmac_key, False)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo>
|
||||
|
||||
<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>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="payment_acquirer_form" model="ir.ui.view">
|
||||
<field name="name">Adyen Acquirer Form</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
|
||||
<record id="payment_provider_form" model="ir.ui.view">
|
||||
<field name="name">Adyen Provider Form</field>
|
||||
<field name="model">payment.provider</field>
|
||||
<field name="inherit_id" ref="payment.payment_provider_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//group[@name="acquirer"]' position='inside'>
|
||||
<group attrs="{'invisible': [('provider', '!=', 'adyen')]}">
|
||||
<field name="adyen_merchant_account" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="adyen_api_key" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}" password="True"/>
|
||||
<field name="adyen_client_key" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="adyen_hmac_key" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}" password="True"/>
|
||||
<field name="adyen_checkout_api_url" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="adyen_recurring_api_url" attrs="{'required':[('provider', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
|
||||
<group name="code" position='inside'>
|
||||
<group attrs="{'invisible': [('code', '!=', 'adyen')]}">
|
||||
<field name="adyen_merchant_account" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="adyen_api_key" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}" password="True"/>
|
||||
<field name="adyen_client_key" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="adyen_hmac_key" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}" password="True"/>
|
||||
<field name="adyen_checkout_api_url" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="adyen_recurring_api_url" attrs="{'required':[('code', '=', 'adyen'), ('state', '!=', 'disabled')]}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr='//group[@name="acquirer_config"]' position='before'>
|
||||
<div attrs="{'invisible':['|', ('provider', '!=', 'adyen'), ('capture_manually', '=', False)]}"
|
||||
</group>
|
||||
<group name="provider_config" position='before'>
|
||||
<div attrs="{'invisible':['|', ('code', '!=', 'adyen'), ('capture_manually', '=', False)]}"
|
||||
class="alert alert-warning"
|
||||
role="alert">
|
||||
<strong>Warning:</strong> To capture the amount manually, you also need to set
|
||||
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"
|
||||
target="_blank">Learn More</a>
|
||||
</div>
|
||||
</xpath>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ from . import models
|
||||
from odoo.exceptions import UserError
|
||||
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):
|
||||
if not any(config.get(key) for key in ('init', 'update')):
|
||||
raise UserError(
|
||||
"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):
|
||||
@@ -21,4 +21,4 @@ def post_init_hook(cr, registry):
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
reset_payment_acquirer(cr, registry, 'alipay')
|
||||
reset_payment_provider(cr, registry, 'alipay')
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Alipay Payment Acquirer',
|
||||
'category': 'Accounting/Payment Acquirers',
|
||||
'name': 'Alipay Payment Provider',
|
||||
'category': 'Accounting/Payment Providers',
|
||||
'version': '2.0',
|
||||
'sequence': 345,
|
||||
'summary': "This module is deprecated.",
|
||||
@@ -10,7 +10,7 @@
|
||||
'data': [
|
||||
'views/payment_alipay_templates.xml',
|
||||
'views/payment_views.xml',
|
||||
'data/payment_acquirer_data.xml',
|
||||
'data/payment_provider_data.xml',
|
||||
],
|
||||
'application': False,
|
||||
'pre_init_hook': 'pre_init_hook',
|
||||
|
||||
@@ -76,10 +76,10 @@ class AlipayController(http.Controller):
|
||||
:return: None
|
||||
: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 = {
|
||||
'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'],
|
||||
}
|
||||
try:
|
||||
@@ -116,7 +116,7 @@ class AlipayController(http.Controller):
|
||||
raise Forbidden()
|
||||
|
||||
# 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):
|
||||
_logger.warning("received notification with invalid signature")
|
||||
raise Forbidden()
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="display_as">Credit Card (powered by Alipay)</field>
|
||||
<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_visa'),
|
||||
])]"/>
|
||||
<field name="provider">alipay</field>
|
||||
<field name="code">alipay</field>
|
||||
<field name="redirect_form_view_id" ref="redirect_form"/>
|
||||
</record>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import payment_acquirer
|
||||
from . import payment_provider
|
||||
from . import payment_transaction
|
||||
|
||||
+10
-10
@@ -8,10 +8,10 @@ from odoo import api, fields, models
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentAcquirer(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
class PaymentProvider(models.Model):
|
||||
_inherit = 'payment.provider'
|
||||
|
||||
provider = fields.Selection(
|
||||
code = fields.Selection(
|
||||
selection_add=[('alipay', "Alipay")], ondelete={'alipay': 'set default'})
|
||||
alipay_payment_method = fields.Selection(
|
||||
string="Account",
|
||||
@@ -33,24 +33,24 @@ class PaymentAcquirer(models.Model):
|
||||
def _compute_feature_support_fields(self):
|
||||
""" Override of `payment` to enable additional features. """
|
||||
super()._compute_feature_support_fields()
|
||||
self.filtered(lambda acq: acq.provider == 'alipay').update({
|
||||
self.filtered(lambda p: p.code == 'alipay').update({
|
||||
'support_fees': True,
|
||||
})
|
||||
|
||||
# === BUSINESS METHODS ===#
|
||||
|
||||
@api.model
|
||||
def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs):
|
||||
""" Override of payment to unlist Alipay acquirers for unsupported currencies. """
|
||||
acquirers = super()._get_compatible_acquirers(*args, currency_id=currency_id, **kwargs)
|
||||
def _get_compatible_providers(self, *args, currency_id=None, **kwargs):
|
||||
""" Override of payment to unlist Alipay providers for unsupported currencies. """
|
||||
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs)
|
||||
|
||||
currency = self.env['res.currency'].browse(currency_id).exists()
|
||||
if currency and currency.name != 'CNY':
|
||||
acquirers = acquirers.filtered(
|
||||
lambda a: a.provider != 'alipay' or a.alipay_payment_method != 'express_checkout'
|
||||
providers = providers.filtered(
|
||||
lambda p: p.code != 'alipay' or p.alipay_payment_method != 'express_checkout'
|
||||
)
|
||||
|
||||
return acquirers
|
||||
return providers
|
||||
|
||||
def _alipay_compute_signature(self, data):
|
||||
# Rearrange parameters in the data set alphabetically
|
||||
@@ -22,14 +22,14 @@ class PaymentTransaction(models.Model):
|
||||
Note: self.ensure_one() from `_get_processing_values`
|
||||
|
||||
: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
|
||||
"""
|
||||
res = super()._get_specific_rendering_values(processing_values)
|
||||
if self.provider != 'alipay':
|
||||
if self.provider_code != 'alipay':
|
||||
return res
|
||||
|
||||
base_url = self.acquirer_id.get_base_url()
|
||||
base_url = self.provider_id.get_base_url()
|
||||
if self.fees:
|
||||
# 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.
|
||||
@@ -41,12 +41,12 @@ class PaymentTransaction(models.Model):
|
||||
'_input_charset': 'utf-8',
|
||||
'notify_url': urls.url_join(base_url, AlipayController._webhook_url),
|
||||
'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),
|
||||
'subject': self.reference,
|
||||
'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
|
||||
rendering_values.update({
|
||||
'service': 'create_forex_trade',
|
||||
@@ -57,29 +57,29 @@ class PaymentTransaction(models.Model):
|
||||
rendering_values.update({
|
||||
'service': 'create_direct_pay_by_user',
|
||||
'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({
|
||||
'sign_type': 'MD5',
|
||||
'sign': sign,
|
||||
'api_url': self.acquirer_id._alipay_get_api_url(),
|
||||
'api_url': self.provider_id._alipay_get_api_url(),
|
||||
})
|
||||
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.
|
||||
|
||||
: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
|
||||
:return: The transaction if found
|
||||
:rtype: recordset of `payment.transaction`
|
||||
:raise: ValidationError if inconsistent data were received
|
||||
:raise: ValidationError if the data match no transaction
|
||||
"""
|
||||
tx = super()._get_tx_from_notification_data(provider, notification_data)
|
||||
if provider != 'alipay' or len(tx) == 1:
|
||||
tx = super()._get_tx_from_notification_data(provider_code, notification_data)
|
||||
if provider_code != 'alipay' or len(tx) == 1:
|
||||
return tx
|
||||
|
||||
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:
|
||||
raise ValidationError(
|
||||
"Alipay: " + _("No transaction found matching reference %s.", reference)
|
||||
@@ -110,10 +110,10 @@ class PaymentTransaction(models.Model):
|
||||
:raise: ValidationError if inconsistent data were received
|
||||
"""
|
||||
super()._process_notification_data(notification_data)
|
||||
if self.provider != 'alipay':
|
||||
if self.provider_code != 'alipay':
|
||||
return
|
||||
|
||||
self.acquirer_reference = notification_data.get('trade_no')
|
||||
self.provider_reference = notification_data.get('trade_no')
|
||||
status = notification_data.get('trade_status')
|
||||
if status in ['TRADE_FINISHED', 'TRADE_SUCCESS']:
|
||||
self._set_done()
|
||||
|
||||
@@ -10,7 +10,7 @@ class AlipayCommon(PaymentCommon):
|
||||
super().setUpClass()
|
||||
|
||||
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_md5_signature_key': 'dummy',
|
||||
'alipay_seller_email': 'dummy',
|
||||
@@ -18,7 +18,7 @@ class AlipayCommon(PaymentCommon):
|
||||
})
|
||||
|
||||
# override defaults for helpers
|
||||
cls.acquirer = cls.alipay
|
||||
cls.provider = cls.alipay
|
||||
cls.currency = cls.currency_yuan
|
||||
|
||||
cls.notification_data = {
|
||||
|
||||
@@ -16,26 +16,26 @@ from odoo.addons.payment_alipay.tests.common import AlipayCommon
|
||||
@tagged('post_install', '-at_install')
|
||||
class AlipayTest(AlipayCommon, PaymentHttpCommon):
|
||||
|
||||
def test_compatible_acquirers(self):
|
||||
def test_compatible_providers(self):
|
||||
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.assertIn(self.alipay, acquirers)
|
||||
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
|
||||
self.assertIn(self.alipay, providers)
|
||||
providers = self.env['payment.provider']._get_compatible_providers(
|
||||
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'
|
||||
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.assertIn(self.alipay, acquirers)
|
||||
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
|
||||
self.assertIn(self.alipay, providers)
|
||||
providers = self.env['payment.provider']._get_compatible_providers(
|
||||
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):
|
||||
self.alipay.alipay_payment_method = 'standard_checkout'
|
||||
@@ -92,7 +92,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
|
||||
)
|
||||
|
||||
def test_03_redirect_form_with_fees(self):
|
||||
# update acquirer: compute fees
|
||||
# update provider: compute fees
|
||||
self.alipay.write({
|
||||
'fees_active': True,
|
||||
'fees_dom_fixed': 1.0,
|
||||
@@ -141,7 +141,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
|
||||
tx = self._create_transaction('redirect')
|
||||
self.env['payment.transaction']._handle_notification_data('alipay', self.notification_data)
|
||||
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
|
||||
self.reference = 'Test Transaction 2'
|
||||
@@ -155,7 +155,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
|
||||
@mute_logger('odoo.addons.payment_alipay.controllers.main')
|
||||
def test_webhook_notification_confirms_transaction(self):
|
||||
""" 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')
|
||||
url = self._build_url(AlipayController._webhook_url)
|
||||
with patch(
|
||||
@@ -171,7 +171,7 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
|
||||
@mute_logger('odoo.addons.payment_alipay.controllers.main')
|
||||
def test_webhook_notification_triggers_origin_and_signature_checks(self):
|
||||
""" 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')
|
||||
url = self._build_url(AlipayController._webhook_url)
|
||||
with patch(
|
||||
@@ -210,8 +210,8 @@ class AlipayTest(AlipayCommon, PaymentHttpCommon):
|
||||
self.assertRaises(Forbidden, AlipayController._verify_notification_signature, payload, tx)
|
||||
|
||||
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.acquirer.alipay_md5_signature_key, False)
|
||||
self.assertEqual(self.acquirer.alipay_seller_email, False)
|
||||
self.assertEqual(self.provider.alipay_merchant_partner_id, False)
|
||||
self.assertEqual(self.provider.alipay_md5_signature_key, False)
|
||||
self.assertEqual(self.provider.alipay_seller_email, False)
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="payment_acquirer_form" model="ir.ui.view">
|
||||
<field name="name">Alipay Acquirer Form</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
|
||||
<record id="payment_provider_form" model="ir.ui.view">
|
||||
<field name="name">Alipay Provider Form</field>
|
||||
<field name="model">payment.provider</field>
|
||||
<field name="inherit_id" ref="payment.payment_provider_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='provider_creation_warning']" position="after">
|
||||
<div class="alert alert-danger"
|
||||
role="alert"
|
||||
attrs="{'invisible': [('provider', '!=', 'alipay')]}">
|
||||
attrs="{'invisible': [('code', '!=', 'alipay')]}">
|
||||
This provider is deprecated.
|
||||
Consider disabling it and moving to <strong>Asiapay</strong>.
|
||||
</div>
|
||||
</xpath>
|
||||
<group name="acquirer" position="inside">
|
||||
<group attrs="{'invisible': [('provider', '!=', 'alipay')]}">
|
||||
<group name="code" position="inside">
|
||||
<group attrs="{'invisible': [('code', '!=', 'alipay')]}">
|
||||
<field name="alipay_payment_method" widget="radio"/>
|
||||
<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_md5_signature_key" password="True"/>
|
||||
</group>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from . import controllers
|
||||
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):
|
||||
@@ -11,4 +11,4 @@ def post_init_hook(cr, registry):
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
reset_payment_acquirer(cr, registry, 'aps')
|
||||
reset_payment_provider(cr, registry, 'aps')
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 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',
|
||||
'category': 'Accounting/Payment Acquirers',
|
||||
'category': 'Accounting/Payment Providers',
|
||||
'sequence': 347,
|
||||
'summary': "Amazon Payment Services' online payment provider covering the MENA region.",
|
||||
'depends': ['payment'],
|
||||
@@ -11,7 +11,7 @@
|
||||
'views/payment_aps_templates.xml',
|
||||
'views/payment_views.xml',
|
||||
|
||||
'data/payment_acquirer_data.xml',
|
||||
'data/payment_provider_data.xml',
|
||||
],
|
||||
'application': True,
|
||||
'post_init_hook': 'post_init_hook',
|
||||
|
||||
@@ -87,7 +87,7 @@ class APSController(http.Controller):
|
||||
raise Forbidden()
|
||||
|
||||
# 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
|
||||
)
|
||||
if not hmac.compare_digest(received_signature, expected_signature):
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="payment.payment_acquirer_aps" model="payment.acquirer">
|
||||
<field name="provider">aps</field>
|
||||
<record id="payment.payment_provider_aps" model="payment.provider">
|
||||
<field name="code">aps</field>
|
||||
<field name="redirect_form_view_id" ref="redirect_form"/>
|
||||
</record>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import payment_acquirer
|
||||
from . import payment_provider
|
||||
from . import payment_transaction
|
||||
|
||||
+4
-4
@@ -9,15 +9,15 @@ from odoo import fields, models
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentAcquirer(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
class PaymentProvider(models.Model):
|
||||
_inherit = 'payment.provider'
|
||||
|
||||
provider = fields.Selection(
|
||||
code = fields.Selection(
|
||||
selection_add=[('aps', "Amazon Payment Services")], ondelete={'aps': 'set default'}
|
||||
)
|
||||
aps_merchant_identifier = fields.Char(
|
||||
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',
|
||||
)
|
||||
aps_access_code = fields.Char(
|
||||
@@ -19,7 +19,7 @@ class PaymentTransaction(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
@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.
|
||||
|
||||
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
|
||||
(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 separator: The custom separator used to separate the prefix from the suffix.
|
||||
:return: The unique reference for the transaction.
|
||||
:rtype: str
|
||||
"""
|
||||
if provider == 'aps':
|
||||
if provider_code == 'aps':
|
||||
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):
|
||||
""" 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`
|
||||
|
||||
: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
|
||||
"""
|
||||
res = super()._get_specific_rendering_values(processing_values)
|
||||
if self.provider != 'aps':
|
||||
if self.provider_code != 'aps':
|
||||
return res
|
||||
|
||||
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 = {
|
||||
'command': 'PURCHASE',
|
||||
'access_code': self.acquirer_id.aps_access_code,
|
||||
'merchant_identifier': self.acquirer_id.aps_merchant_identifier,
|
||||
'access_code': self.provider_id.aps_access_code,
|
||||
'merchant_identifier': self.provider_id.aps_merchant_identifier,
|
||||
'merchant_reference': self.reference,
|
||||
'amount': str(converted_amount),
|
||||
'currency': self.currency_id.name,
|
||||
@@ -66,25 +66,25 @@ class PaymentTransaction(models.Model):
|
||||
'return_url': urls.url_join(base_url, APSController._return_url),
|
||||
}
|
||||
rendering_values.update({
|
||||
'signature': self.acquirer_id._aps_calculate_signature(
|
||||
'signature': self.provider_id._aps_calculate_signature(
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
: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.
|
||||
:return: The transaction if found.
|
||||
:rtype: recordset of `payment.transaction`
|
||||
:raise ValidationError: If inconsistent data are received.
|
||||
:raise ValidationError: If the data match no transaction.
|
||||
"""
|
||||
tx = super()._get_tx_from_notification_data(provider, notification_data)
|
||||
if provider != 'aps' or len(tx) == 1:
|
||||
tx = super()._get_tx_from_notification_data(provider_code, notification_data)
|
||||
if provider_code != 'aps' or len(tx) == 1:
|
||||
return tx
|
||||
|
||||
reference = notification_data.get('merchant_reference')
|
||||
@@ -93,7 +93,7 @@ class PaymentTransaction(models.Model):
|
||||
"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:
|
||||
raise ValidationError(
|
||||
"APS: " + _("No transaction found matching reference %s.", reference)
|
||||
@@ -111,10 +111,10 @@ class PaymentTransaction(models.Model):
|
||||
:raise ValidationError: If inconsistent data are received.
|
||||
"""
|
||||
super()._process_notification_data(notification_data)
|
||||
if self.provider != 'aps':
|
||||
if self.provider_code != 'aps':
|
||||
return
|
||||
|
||||
self.acquirer_reference = notification_data.get('fort_id')
|
||||
self.provider_reference = notification_data.get('fort_id')
|
||||
|
||||
status = notification_data.get('status')
|
||||
if not status:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import test_payment_acquirer
|
||||
from . import test_payment_provider
|
||||
from . import test_payment_transaction
|
||||
from . import test_processing_flows
|
||||
|
||||
@@ -9,17 +9,17 @@ class APSCommon(PaymentHttpCommon):
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.aps = cls._prepare_acquirer('aps', update_values={
|
||||
cls.aps = cls._prepare_provider('aps', update_values={
|
||||
'aps_merchant_identifier': '123456abc',
|
||||
'aps_access_code': 'dummy',
|
||||
'aps_sha_request': 'dummy',
|
||||
'aps_sha_response': 'dummy',
|
||||
})
|
||||
|
||||
cls.acquirer = cls.aps
|
||||
cls.provider = cls.aps
|
||||
|
||||
cls.notification_data = {
|
||||
'access_code': cls.acquirer.aps_access_code,
|
||||
'access_code': cls.provider.aps_access_code,
|
||||
'amount': cls.amount,
|
||||
'authorization_code': '123456',
|
||||
'card_holder_name': 'Mitchell',
|
||||
@@ -32,7 +32,7 @@ class APSCommon(PaymentHttpCommon):
|
||||
'expiry_date': '2212',
|
||||
'fort_id': '169996210006464984',
|
||||
'language': 'en',
|
||||
'merchant_identifier': cls.acquirer.aps_merchant_identifier,
|
||||
'merchant_identifier': cls.provider.aps_merchant_identifier,
|
||||
'merchant_reference': cls.reference,
|
||||
'payment_option': 'VISA',
|
||||
'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)
|
||||
expected_values = {
|
||||
'command': 'PURCHASE',
|
||||
'access_code': self.acquirer.aps_access_code,
|
||||
'merchant_identifier': self.acquirer.aps_merchant_identifier,
|
||||
'access_code': self.provider.aps_access_code,
|
||||
'merchant_identifier': self.provider.aps_merchant_identifier,
|
||||
'merchant_reference': tx.reference,
|
||||
'amount': str(converted_amount),
|
||||
'currency': self.currency.name,
|
||||
@@ -33,7 +33,7 @@ class TestPaymentTransaction(APSCommon):
|
||||
'customer_email': tx.partner_id.email_normalized,
|
||||
'return_url': self._build_url(APSController._return_url),
|
||||
'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)
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="payment_acquirer_form" model="ir.ui.view">
|
||||
<field name="name">APS Acquirer Form</field>
|
||||
<field name="model">payment.acquirer</field>
|
||||
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
|
||||
<record id="payment_provider_form" model="ir.ui.view">
|
||||
<field name="name">APS Provider Form</field>
|
||||
<field name="model">payment.provider</field>
|
||||
<field name="inherit_id" ref="payment.payment_provider_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="acquirer" position='inside'>
|
||||
<group attrs="{'invisible': [('provider', '!=', 'aps')]}">
|
||||
<group name="code" position='inside'>
|
||||
<group attrs="{'invisible': [('code', '!=', 'aps')]}">
|
||||
<field name="aps_merchant_identifier"
|
||||
string="Merchant Identifier"
|
||||
attrs="{'required': [('provider', '=', 'aps'), ('state', '!=', 'disabled')]}"/>
|
||||
attrs="{'required': [('code', '=', 'aps'), ('state', '!=', 'disabled')]}"/>
|
||||
<field name="aps_access_code"
|
||||
string="Access Code"
|
||||
attrs="{'required': [('provider', '=', 'aps'), ('state', '!=', 'disabled')]}"
|
||||
attrs="{'required': [('code', '=', 'aps'), ('state', '!=', 'disabled')]}"
|
||||
password="True"/>
|
||||
<field name="aps_sha_request"
|
||||
string="SHA Request Phrase"
|
||||
attrs="{'required': [('provider', '=', 'aps'), ('state', '!=', 'disabled')]}"
|
||||
attrs="{'required': [('code', '=', 'aps'), ('state', '!=', 'disabled')]}"
|
||||
password="True"/>
|
||||
<field name="aps_sha_response"
|
||||
string="SHA Response Phrase"
|
||||
attrs="{'required': [('provider', '=', 'aps'), ('state', '!=', 'disabled')]}"
|
||||
attrs="{'required': [('code', '=', 'aps'), ('state', '!=', 'disabled')]}"
|
||||
password="True"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user