Skip to content

Commit

Permalink
[IMP] payment: allow the public user to pay with tokens
Browse files Browse the repository at this point in the history
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#104472

Related: odoo/enterprise#34792
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
  • Loading branch information
hote-odoo committed Mar 8, 2023
1 parent 9461d2d commit ddda5d4
Show file tree
Hide file tree
Showing 9 changed files with 51 additions and 45 deletions.
10 changes: 2 additions & 8 deletions addons/account_payment/controllers/portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def _invoice_get_page_view_values(self, invoice, access_token, **kwargs):
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.
Expand All @@ -54,7 +54,7 @@ def _invoice_get_page_view_values(self, invoice, access_token, **kwargs):
'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,
Expand All @@ -64,10 +64,4 @@ def _invoice_get_page_view_values(self, invoice, access_token, **kwargs):
'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
5 changes: 0 additions & 5 deletions addons/account_payment/views/account_portal_templates.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
12 changes: 5 additions & 7 deletions addons/payment/controllers/portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def payment_pay(
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.
Expand All @@ -138,7 +138,7 @@ def payment_pay(
'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,
Expand All @@ -154,22 +154,20 @@ def payment_pay(
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
"""
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

Expand All @@ -196,7 +194,7 @@ def payment_method(self, **kwargs):
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,
Expand Down
19 changes: 10 additions & 9 deletions addons/payment/models/payment_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -74,6 +74,13 @@ def write(self, values):

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.
Expand All @@ -89,9 +96,7 @@ def name_get(self):

#=== 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
Expand All @@ -100,14 +105,10 @@ def _get_available_tokens(
: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)]
Expand Down
27 changes: 17 additions & 10 deletions addons/payment/tests/test_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,16 +379,23 @@ def test_payment_by_token_triggers_exactly_one_payment_request(self):
)
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})
8 changes: 7 additions & 1 deletion addons/payment/tests/test_payment_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -20,6 +20,12 @@ def test_users_have_no_access_to_other_users_tokens(self):
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)
Expand Down
5 changes: 2 additions & 3 deletions addons/sale/controllers/portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ def _get_payment_values(self, order_sudo, **kwargs):
: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
Expand All @@ -198,11 +197,11 @@ def _get_payment_values(self, order_sudo, **kwargs):
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,
Expand Down
2 changes: 1 addition & 1 deletion addons/sale/tests/test_payment_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_11_so_payment_link(self):
'._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)
Expand Down
8 changes: 7 additions & 1 deletion addons/website_payment/controllers/portal.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ def donation_transaction(self, amount, currency_id, partner_id, access_token, mi
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
)
Expand Down

0 comments on commit ddda5d4

Please sign in to comment.