Odoo18-Base/addons/payment/models/payment_method.py
2025-01-06 10:57:38 +07:00

315 lines
14 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command, _, api, fields, models
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment.const import REPORT_REASONS_MAPPING
class PaymentMethod(models.Model):
_name = 'payment.method'
_description = "Payment Method"
_order = 'active desc, sequence, name'
name = fields.Char(string="Name", required=True, translate=True)
code = fields.Char(
string="Code", help="The technical code of this payment method.", required=True
)
sequence = fields.Integer(string="Sequence", default=1)
primary_payment_method_id = fields.Many2one(
string="Primary Payment Method",
help="The primary payment method of the current payment method, if the latter is a brand."
"\nFor example, \"Card\" is the primary payment method of the card brand \"VISA\".",
comodel_name='payment.method',
)
brand_ids = fields.One2many(
string="Brands",
help="The brands of the payment methods that will be displayed on the payment form.",
comodel_name='payment.method',
inverse_name='primary_payment_method_id',
)
is_primary = fields.Boolean(
string="Is Primary Payment Method",
compute='_compute_is_primary',
search='_search_is_primary',
)
provider_ids = fields.Many2many(
string="Providers",
help="The list of providers supporting this payment method.",
comodel_name='payment.provider',
)
active = fields.Boolean(string="Active", default=True)
image = fields.Image(
string="Image",
help="The base image used for this payment method; in a 64x64 px format.",
max_width=64,
max_height=64,
required=True,
)
image_payment_form = fields.Image(
string="The resized image displayed on the payment form.",
related='image',
store=True,
max_width=45,
max_height=30,
)
# Feature support fields.
support_tokenization = fields.Boolean(
string="Tokenization",
help="Tokenization is the process of saving the payment details as a token that can later"
" be reused without having to enter the payment details again.",
)
support_express_checkout = fields.Boolean(
string="Express Checkout",
help="Express checkout allows customers to pay faster by using a payment method that"
" provides all required billing and shipping information, thus allowing to skip the"
" checkout process.",
)
support_refund = fields.Selection(
string="Refund",
help="Refund is a feature allowing to refund customers directly from the payment in Odoo.",
selection=[
('none', "Unsupported"),
('full_only', "Full Only"),
('partial', "Full & Partial"),
],
required=True,
default='none',
)
supported_country_ids = fields.Many2many(
string="Countries",
comodel_name='res.country',
help="The list of countries in which this payment method can be used (if the provider"
" allows it). In other countries, this payment method is not available to customers."
)
supported_currency_ids = fields.Many2many(
string="Currencies",
comodel_name='res.currency',
help="The list of currencies for that are supported by this payment method (if the provider"
" allows it). When paying with another currency, this payment method is not available "
"to customers.",
context={'active_test': False},
)
#=== COMPUTE METHODS ===#
def _compute_is_primary(self):
for payment_method in self:
payment_method.is_primary = not payment_method.primary_payment_method_id
def _search_is_primary(self, operator, value):
if operator == '=' and value is True:
return [('primary_payment_method_id', '=', False)]
elif operator == '=' and value is False:
return [('primary_payment_method_id', '!=', False)]
else:
raise NotImplementedError(_("Operation not supported."))
#=== ONCHANGE METHODS ===#
@api.onchange('active', 'provider_ids', 'support_tokenization')
def _onchange_warn_before_disabling_tokens(self):
""" Display a warning about the consequences of archiving the payment method, detaching it
from a provider, or removing its support for tokenization.
Let the user know that the related tokens will be archived.
:return: A client action with the warning message, if any.
:rtype: dict
"""
disabling = self._origin.active and not self.active
detached_providers = self._origin.provider_ids.filtered(
lambda p: p.id not in self.provider_ids.ids
) # Cannot use recordset difference operation because self.provider_ids is a set of NewIds.
blocking_tokenization = self._origin.support_tokenization and not self.support_tokenization
if disabling or detached_providers or blocking_tokenization:
related_tokens = self.env['payment.token'].with_context(active_test=True).search(
expression.AND([
[('payment_method_id', 'in', (self._origin + self._origin.brand_ids).ids)],
[('provider_id', 'in', detached_providers.ids)] if detached_providers else [],
])
) # Fix `active_test` in the context forwarded by the view.
if related_tokens:
return {
'warning': {
'title': _("Warning"),
'message': _(
"This action will also archive %s tokens that are registered with this "
"payment method.", len(related_tokens)
)
}
}
@api.onchange('provider_ids')
def _onchange_provider_ids_warn_before_attaching_payment_method(self):
""" Display a warning before attaching a payment method to a provider.
:return: A client action with the warning message, if any.
:rtype: dict
"""
attached_providers = self.provider_ids.filtered(
lambda p: p.id.origin not in self._origin.provider_ids.ids
)
if attached_providers:
return {
'warning': {
'title': _("Warning"),
'message': _(
"Please make sure that %(payment_method)s is supported by %(provider)s.",
payment_method=self.name,
provider=', '.join(attached_providers.mapped('name'))
)
}
}
#=== CRUD METHODS ===#
def write(self, values):
# Handle payment methods being archived, detached from providers, or blocking tokenization.
archiving = values.get('active') is False
detached_provider_ids = [
vals[0] for command, *vals in values['provider_ids'] if command == Command.UNLINK
] if 'provider_ids' in values else []
blocking_tokenization = values.get('support_tokenization') is False
if archiving or detached_provider_ids or blocking_tokenization:
linked_tokens = self.env['payment.token'].with_context(active_test=True).search(
expression.AND([
[('payment_method_id', 'in', (self + self.brand_ids).ids)],
[('provider_id', 'in', detached_provider_ids)] if detached_provider_ids else [],
])
) # Fix `active_test` in the context forwarded by the view.
linked_tokens.active = False
# Prevent enabling a payment method if it is not linked to an enabled provider.
if values.get('active'):
for pm in self:
primary_pm = pm if pm.is_primary else pm.primary_payment_method_id
if (
not primary_pm.active # Don't bother for already enabled payment methods.
and all(p.state == 'disabled' for p in primary_pm.provider_ids)
):
raise UserError(_(
"This payment method needs a partner in crime; you should enable a payment"
" provider supporting this method first."
))
return super().write(values)
# === BUSINESS METHODS === #
def _get_compatible_payment_methods(
self, provider_ids, partner_id, currency_id=None, force_tokenization=False,
is_express_checkout=False, report=None, **kwargs
):
""" Search and return the payment methods matching the compatibility criteria.
The compatibility criteria are that payment methods must: be supported by at least one of
the providers; support the country of the partner if it exists; be primary payment methods
(not a brand). If provided, the optional keyword arguments further refine the criteria.
:param list provider_ids: The list of providers by which the payment methods must be at
least partially supported to be considered compatible, as a list
of `payment.provider` ids.
:param int partner_id: The partner making the payment, as a `res.partner` id.
:param int currency_id: The payment currency, if known beforehand, as a `res.currency` id.
:param bool force_tokenization: Whether only payment methods supporting tokenization can be
matched.
:param bool is_express_checkout: Whether the payment is made through express checkout.
:param dict report: The report in which each provider's availability status and reason must
be logged.
:param dict kwargs: Optional data. This parameter is not used here.
:return: The compatible payment methods.
:rtype: payment.method
"""
# Search compatible payment methods with the base domain.
payment_methods = self.env['payment.method'].search([('is_primary', '=', True)])
payment_utils.add_to_report(report, payment_methods)
# Filter by compatible providers.
unfiltered_pms = payment_methods
payment_methods = payment_methods.filtered(
lambda pm: any(p in provider_ids for p in pm.provider_ids.ids)
)
payment_utils.add_to_report(
report,
unfiltered_pms - payment_methods,
available=False,
reason=REPORT_REASONS_MAPPING['provider_not_available'],
)
# Handle the partner country; allow all countries if the list is empty.
partner = self.env['res.partner'].browse(partner_id)
if partner.country_id: # The partner country must either not be set or be supported.
unfiltered_pms = payment_methods
payment_methods = payment_methods.filtered(
lambda pm: (
not pm.supported_country_ids
or partner.country_id.id in pm.supported_country_ids.ids
)
)
payment_utils.add_to_report(
report,
unfiltered_pms - payment_methods,
available=False,
reason=REPORT_REASONS_MAPPING['incompatible_country'],
)
# Handle the supported currencies; allow all currencies if the list is empty.
if currency_id:
unfiltered_pms = payment_methods
payment_methods = payment_methods.filtered(
lambda pm: (
not pm.supported_currency_ids
or currency_id in pm.supported_currency_ids.ids
)
)
payment_utils.add_to_report(
report,
unfiltered_pms - payment_methods,
available=False,
reason=REPORT_REASONS_MAPPING['incompatible_currency'],
)
# Handle tokenization support requirements.
if force_tokenization:
unfiltered_pms = payment_methods
payment_methods = payment_methods.filtered('support_tokenization')
payment_utils.add_to_report(
report,
unfiltered_pms - payment_methods,
available=False,
reason=REPORT_REASONS_MAPPING['tokenization_not_supported'],
)
# Handle express checkout.
if is_express_checkout:
unfiltered_pms = payment_methods
payment_methods = payment_methods.filtered('support_express_checkout')
payment_utils.add_to_report(
report,
unfiltered_pms - payment_methods,
available=False,
reason=REPORT_REASONS_MAPPING['express_checkout_not_supported'],
)
return payment_methods
def _get_from_code(self, code, mapping=None):
""" Get the payment method corresponding to the given provider-specific code.
If a mapping is given, the search uses the generic payment method code that corresponds to
the given provider-specific code.
:param str code: The provider-specific code of the payment method to get.
:param dict mapping: A non-exhaustive mapping of generic payment method codes to
provider-specific codes.
:return: The corresponding payment method, if any.
:type: payment.method
"""
generic_to_specific_mapping = mapping or {}
specific_to_generic_mapping = {v: k for k, v in generic_to_specific_mapping.items()}
return self.search([('code', '=', specific_to_generic_mapping.get(code, code))], limit=1)