[IMP] payment: allow the public user to pay with tokens

Before this commit the public user did not have access to tokens or the
possibility of saving payment methods.

When receiving a link to pay the customer (even if not logged in) should
be able to use tokens saved by the parter of the document and also save
new payment methods. This is intuitively correct: as the possesor of the
link, the customer have rights to pay with tokens linked to the partner.

After this commit tokens linked to the partner of the document will be
visible to the public user and also the possibily to save payment
methods.

Task - 2799296

closes odoo/odoo#104472

Related: odoo/enterprise#34792
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Horacio Tellez
2023-03-08 11:36:53 +01:00
parent 9461d2d52b
commit ddda5d4a26
9 changed files with 51 additions and 45 deletions
+2 -8
View File
@@ -31,7 +31,7 @@ class PortalAccount(portal.PortalAccount):
currency_id=invoice.currency_id.id
) # In sudo mode to read the fields of providers and partner (if not logged in)
tokens = request.env['payment.token']._get_available_tokens(
providers_sudo.ids, partner_id=partner_sudo.id, logged_in=logged_in
providers_sudo.ids, partner_sudo.id
)
# Make sure that the partner's company matches the invoice's company.
@@ -54,7 +54,7 @@ class PortalAccount(portal.PortalAccount):
'tokens': tokens,
'fees_by_provider': fees_by_provider,
'show_tokenize_input': PaymentPortal._compute_show_tokenize_input_mapping(
providers_sudo, logged_in=logged_in
providers_sudo
),
'amount': invoice.amount_residual,
'currency': invoice.currency_id,
@@ -64,10 +64,4 @@ class PortalAccount(portal.PortalAccount):
'landing_route': _build_url_w_params(invoice.access_url, {'access_token': access_token})
}
values.update(**portal_page_values, **payment_form_values)
if not logged_in:
# Don't display payment tokens of the invoice partner if the user is not logged in, but
# inform that logging in will make them available.
values.update({
'existing_token': bool(tokens),
})
return values
@@ -93,11 +93,6 @@
<div t-if="(invoice.amount_residual or not tx_ids) and invoice.state == 'posted' and invoice.payment_state in ('not_paid', 'partial') and invoice.amount_total" id="portal_pay">
<t t-call="account_payment.portal_invoice_payment"/>
</div>
<div class="panel-body" t-if="existing_token">
<div class="offset-lg-3 col-lg-6">
<i class="fa fa-info"></i> You have credits card registered, you can log-in to be able to use them.
</div>
</div>
</xpath>
</template>
+5 -7
View File
@@ -113,7 +113,7 @@ class PaymentPortal(portal.CustomerPortal):
if provider_id in providers_sudo.ids: # Only keep the desired provider if it's suitable
providers_sudo = providers_sudo.browse(provider_id)
tokens_sudo = request.env['payment.token'].sudo()._get_available_tokens(
providers_sudo.ids, partner_id=partner_sudo.id, logged_in=logged_in
providers_sudo.ids, partner_sudo.id
) # In sudo mode to be able to read tokens of other partners.
# Make sure that the partner's company matches the company passed as parameter.
@@ -138,7 +138,7 @@ class PaymentPortal(portal.CustomerPortal):
'tokens': tokens_sudo,
'fees_by_provider': fees_by_provider,
'show_tokenize_input': self._compute_show_tokenize_input_mapping(
providers_sudo, logged_in=logged_in, **kwargs
providers_sudo, **kwargs
),
'reference_prefix': reference,
'amount': amount,
@@ -154,13 +154,12 @@ class PaymentPortal(portal.CustomerPortal):
return request.render(self._get_payment_page_template_xmlid(**kwargs), rendering_context)
@staticmethod
def _compute_show_tokenize_input_mapping(providers_sudo, logged_in=False, **kwargs):
def _compute_show_tokenize_input_mapping(providers_sudo, **kwargs):
""" Determine for each provider whether the tokenization input should be shown or not.
:param recordset providers_sudo: The providers for which to determine whether the
tokenization input should be shown or not, as a sudoed
`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 provider id.
:rtype: dict
@@ -168,8 +167,7 @@ class PaymentPortal(portal.CustomerPortal):
show_tokenize_input_mapping = {}
for provider_sudo in providers_sudo:
show_tokenize_input = provider_sudo.allow_tokenization \
and not provider_sudo._is_tokenization_required(**kwargs) \
and logged_in
and not provider_sudo._is_tokenization_required(**kwargs)
show_tokenize_input_mapping[provider_sudo.id] = show_tokenize_input
return show_tokenize_input_mapping
@@ -196,7 +194,7 @@ class PaymentPortal(portal.CustomerPortal):
rendering_context = {
'providers': providers_sudo,
'tokens': request.env['payment.token'].sudo()._get_available_tokens(
None, partner_id=partner_sudo.id, is_validation=True, logged_in=True
None, partner_sudo.id, is_validation=True
), # In sudo mode to read the commercial partner's fields.
'reference_prefix': payment_utils.singularize_reference_prefix(prefix='V'),
'partner_id': partner_sudo.id,
+10 -9
View File
@@ -3,7 +3,7 @@
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.exceptions import UserError, ValidationError
class PaymentToken(models.Model):
@@ -74,6 +74,13 @@ class PaymentToken(models.Model):
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.
@@ -89,9 +96,7 @@ class PaymentToken(models.Model):
#=== BUSINESS METHODS ===#
def _get_available_tokens(
self, providers_ids, partner_id=None, is_validation=False, logged_in=False, **kwargs
):
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
@@ -100,14 +105,10 @@ class PaymentToken(models.Model):
: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 bool logged_in: Whether the user is logged in.
:param dict kwargs: Locally unused keywords arguments.
:return: The available tokens.
:rtype: `payment.token`
:rtype: payment.token
"""
if partner_id is None or not logged_in:
return self.env['payment.token']
if not is_validation:
return self.env['payment.token'].search(
[('provider_id', 'in', providers_ids), ('partner_id', '=', partner_id)]
+17 -10
View File
@@ -379,16 +379,23 @@ class TestFlows(PaymentHttpCommon):
)
self.assertEqual(patched.call_count, 1)
def test_tokenization_input_is_show_to_logged_in_users(self):
def test_tokenization_input_is_shown_to_logged_in_users(self):
# Test both for portal and internal users
self.user = self.portal_user
self.provider.allow_tokenization = True
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(
self.provider, logged_in=True
)
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(self.provider)
self.assertDictEqual(show_tokenize_input, {self.provider.id: True})
def test_tokenization_input_is_hidden_for_logged_out_users(self):
self.provider.allow_tokenization = False
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(
self.provider, logged_in=True
)
self.assertDictEqual(show_tokenize_input, {self.provider.id: False})
self.user = self.internal_user
self.provider.allow_tokenization = True
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(self.provider)
self.assertDictEqual(show_tokenize_input, {self.provider.id: True})
def test_tokenization_input_is_shown_to_logged_out_users(self):
self.user = self.public_user
self.provider.allow_tokenization = True
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(self.provider)
self.assertDictEqual(show_tokenize_input, {self.provider.id: True})
+7 -1
View File
@@ -2,7 +2,7 @@
from datetime import date
from odoo.exceptions import AccessError, UserError
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests import tagged
from odoo.tools import mute_logger
@@ -20,6 +20,12 @@ class TestPaymentToken(PaymentCommon):
with self.assertRaises(AccessError):
token.with_user(user).read()
def test_cannot_assign_token_to_public_partner(self):
""" Test that no token can be assigned to the public partner. """
token = self._create_token()
with self.assertRaises(ValidationError):
token.partner_id = self.public_user.partner_id
def test_token_cannot_be_unarchived(self):
""" Test that unarchiving disabled tokens is forbidden. """
token = self._create_token(active=False)
+2 -3
View File
@@ -177,7 +177,6 @@ class CustomerPortal(portal.CustomerPortal):
:return: The payment-specific values.
:rtype: dict
"""
logged_in = not request.env.user._is_public()
partner = order_sudo.partner_id
company = order_sudo.company_id
amount = order_sudo.amount_total
@@ -198,11 +197,11 @@ class CustomerPortal(portal.CustomerPortal):
payment_form_values = {
'providers': providers_sudo,
'tokens': request.env['payment.token']._get_available_tokens(
providers_sudo.ids, partner_id=partner.id, logged_in=logged_in, **kwargs
providers_sudo.ids, partner.id, **kwargs
),
'fees_by_provider': fees_by_provider,
'show_tokenize_input': PaymentPortal._compute_show_tokenize_input_mapping(
providers_sudo, logged_in=logged_in, sale_order_id=order_sudo.id
providers_sudo, sale_order_id=order_sudo.id
),
'amount': amount,
'currency': currency,
+1 -1
View File
@@ -32,7 +32,7 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
'._compute_show_tokenize_input_mapping'
) as patched:
tx_context = self._get_tx_checkout_context(**route_values)
patched.assert_called_once_with(ANY, logged_in=ANY, sale_order_id=ANY)
patched.assert_called_once_with(ANY, sale_order_id=ANY)
self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
self.assertEqual(tx_context['partner_id'], self.sale_order.partner_id.id)
+7 -1
View File
@@ -64,7 +64,13 @@ class PaymentPortal(payment_portal.PaymentPortal):
else:
partner_id = request.env.user.partner_id.id
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
# Don't allow passing arbitrary create values and avoid tokenization for
# the public user.
if use_public_partner:
kwargs['custom_create_values'] = {'tokenize': False}
else:
kwargs.pop('custom_create_values', None)
tx_sudo = self._create_transaction(
amount=amount, currency_id=currency_id, partner_id=partner_id, **kwargs
)