Before this commit, the payment providers (e.g., Stripe, Adyen...) available for payment were displayed on the payment forms. The customer had to select one to process their payment. After that, the customer had to select their preferred payment method (e.g., Credit Card, Bancontact...) from a list of payment methods supported by the selected provider over which the website administrator had close to no control. This was making the payment forms confusing because the payment methods were displayed sometimes more than once, if at all, in a non-controlled order, and behind the selection of a payment provider that customers should not have to deal with. As the payment method was selected in an iframe or directly on the provider's website, the information on the selection payment method was not available in Odoo. This posed many problems, among which were the impossibility of assessing whether a specific feature (e.g., tokenization, refunds, manual capture...) was available, not being able to easily identify payment tokens through the payment method logo, listing available payment methods on the website, sorting and fine-grained configuration of the available payment method, subpar payment method-specific display on the payment form (e.g., PayPal that requires displaying a "Pay with PayPal" button), etc. In this commit, the payment providers are thus replaced by the payment methods on the payment forms. All contextually available (depending on the country, currency, requested feature...) payment methods are displayed one after the other on a single-level list and in the order configured by the website administrator. Each payment method is "powered by" (i.e., linked) to a single payment provider: the first one, by model order, to support it. This allows, for example, offering the PayPal payment method through Mollie, which charges low processing fees, while also offering Klarna through Stripe, which supports more payment methods but charges higher processing fees. While doing so, the two different payment forms, "Checkout" and "Manage", are also merged together in a new, configurable case-by-case, payment form that is entirely redesigned to offer a better user experience. After payment, the information on the selected payment method is saved on the transaction and eventual payment record and updated with the information received from the provider. task-2882677 closes odoo/odoo#120446 Related: odoo/upgrade#5103 Related: odoo/documentation#5717 Related: odoo/enterprise#40666 Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com> Co-authored-by: Anita (anko) <anko@odoo.com> Co-authored-by: Brieuc-brd <brd@odoo.com> Co-authored-by: Valeriya (vchu) <vchu@odoo.com>
194 lines
8.2 KiB
Python
194 lines
8.2 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError, ValidationError
|
|
|
|
|
|
class PaymentToken(models.Model):
|
|
_name = 'payment.token'
|
|
_order = 'partner_id, id desc'
|
|
_description = 'Payment Token'
|
|
_check_company_auto = True
|
|
|
|
provider_id = fields.Many2one(string="Provider", comodel_name='payment.provider', required=True)
|
|
provider_code = fields.Selection(string="Provider Code", related='provider_id.code')
|
|
company_id = fields.Many2one(
|
|
related='provider_id.company_id', store=True, index=True
|
|
) # Indexed to speed-up ORM searches (from ir_rule or others).
|
|
payment_method_id = fields.Many2one(
|
|
string="Payment Method", comodel_name='payment.method', readonly=True, required=True
|
|
)
|
|
payment_method_code = fields.Char(
|
|
string="Payment Method Code", related='payment_method_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)
|
|
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'
|
|
)
|
|
active = fields.Boolean(string="Active", default=True)
|
|
|
|
#=== COMPUTE METHODS ===#
|
|
|
|
@api.depends('payment_details', 'create_date')
|
|
def _compute_display_name(self):
|
|
for token in self:
|
|
token.display_name = token._build_display_name()
|
|
|
|
#=== CRUD METHODS ===#
|
|
|
|
@api.model_create_multi
|
|
def create(self, values_list):
|
|
for values in values_list:
|
|
if 'provider_id' in values:
|
|
provider = self.env['payment.provider'].browse(values['provider_id'])
|
|
|
|
# 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_code, values):
|
|
""" Complete the values of the `create` method with provider-specific values.
|
|
|
|
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_code: The code of the provider managing the token.
|
|
:param dict values: The original create values.
|
|
:return: The dict of provider-specific create values.
|
|
:rtype: dict
|
|
"""
|
|
return dict()
|
|
|
|
def write(self, values):
|
|
""" Prevent unarchiving tokens and handle their archiving.
|
|
|
|
:return: The result of the call to the parent method.
|
|
:rtype: bool
|
|
:raise UserError: If at least one token is being unarchived.
|
|
"""
|
|
if 'active' in values:
|
|
if values['active']:
|
|
if any(not token.active for token in self):
|
|
raise UserError(_("A token cannot be unarchived once it has been archived."))
|
|
else:
|
|
# Call the handlers in sudo mode because this method might have been called by RPC.
|
|
self.filtered('active').sudo()._handle_archiving()
|
|
|
|
return super().write(values)
|
|
|
|
@api.constrains('partner_id')
|
|
def _check_partner_is_never_public(self):
|
|
""" Check that the partner associated with the token is never public. """
|
|
for token in self:
|
|
if token.partner_id.is_public:
|
|
raise ValidationError(_("No token can be assigned to the public partner."))
|
|
|
|
def _handle_archiving(self):
|
|
""" Handle the archiving of tokens.
|
|
|
|
For a module to perform additional operations when a token is archived, it must override
|
|
this method.
|
|
|
|
:return: None
|
|
"""
|
|
return
|
|
|
|
#=== BUSINESS METHODS ===#
|
|
|
|
def _get_available_tokens(self, providers_ids, partner_id, is_validation=False, **kwargs):
|
|
""" Return the available tokens linked to the given providers and partner.
|
|
|
|
For a module to retrieve the available tokens, it must override this method and add
|
|
information in the kwargs to define the context of the request.
|
|
|
|
:param list providers_ids: The ids of the providers available for the transaction.
|
|
:param int partner_id: The id of the partner.
|
|
:param bool is_validation: Whether the transaction is a validation operation.
|
|
:param dict kwargs: Locally unused keywords arguments.
|
|
:return: The available tokens.
|
|
:rtype: payment.token
|
|
"""
|
|
if not is_validation:
|
|
return self.env['payment.token'].search(
|
|
[('provider_id', 'in', providers_ids), ('partner_id', '=', partner_id)]
|
|
)
|
|
else:
|
|
# Get all the tokens of the partner and of their commercial partner, regardless of
|
|
# whether the providers are available.
|
|
partner = self.env['res.partner'].browse(partner_id)
|
|
return self.env['payment.token'].search(
|
|
[('partner_id', 'in', [partner.id, partner.commercial_partner_id.id])]
|
|
)
|
|
|
|
def _build_display_name(self, *args, max_length=34, should_pad=True, **kwargs):
|
|
""" Build a token name of the desired maximum length with the format `•••• 1234`.
|
|
|
|
The payment details are padded on the left with up to four padding characters. The padding
|
|
is only added if there is enough room for it. If not, it is either reduced or not added at
|
|
all. If there is not enough room for the payment details either, they are trimmed from the
|
|
left.
|
|
|
|
For a module to customize the display name of a token, it must override this method and
|
|
return the customized display name.
|
|
|
|
Note: `self.ensure_one()`
|
|
|
|
:param list args: The arguments passed by QWeb when calling this method.
|
|
:param int max_length: The desired maximum length of the token name. The default is `34` to
|
|
fit the largest IBANs.
|
|
:param bool should_pad: Whether the token should be padded.
|
|
:param dict kwargs: Optional data used in overrides of this method.
|
|
:return: The padded token name.
|
|
:rtype: str
|
|
"""
|
|
self.ensure_one()
|
|
|
|
padding_length = max_length - len(self.payment_details or '')
|
|
if not self.payment_details:
|
|
create_date_str = self.create_date.strftime('%Y/%m/%d')
|
|
display_name = _("Payment details saved on %(date)s", date=create_date_str)
|
|
elif padding_length >= 2: # Enough room for padding.
|
|
padding = '•' * min(padding_length - 1, 4) + ' ' if should_pad else ''
|
|
display_name = ''.join([padding, self.payment_details])
|
|
elif padding_length > 0: # Not enough room for padding.
|
|
display_name = self.payment_details
|
|
else: # Not enough room for neither padding nor the payment details.
|
|
display_name = self.payment_details[-max_length:] if max_length > 0 else ''
|
|
return display_name
|
|
|
|
def get_linked_records_info(self):
|
|
""" Return a list of information about records linked to the current token.
|
|
|
|
For a module to implement payments and link documents to a token, it must override this
|
|
method and add information about linked document records to the returned list.
|
|
|
|
The information must be structured as a dict with the following keys:
|
|
|
|
- `description`: The description of the record's model (e.g. "Subscription").
|
|
- `id`: The id of the record.
|
|
- `name`: The name of the record.
|
|
- `url`: The url to access the record.
|
|
|
|
Note: `self.ensure_one()`
|
|
|
|
:return: The list of information about the linked document records.
|
|
:rtype: list
|
|
"""
|
|
self.ensure_one()
|
|
return []
|