1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
|
# 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)
|