123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- import urllib.parse
- import werkzeug
- from odoo import _, http
- from odoo.exceptions import AccessError, UserError, ValidationError
- from odoo.http import request
- from odoo.addons.payment import utils as payment_utils
- from odoo.addons.payment.controllers.post_processing import PaymentPostProcessing
- from odoo.addons.portal.controllers import portal
- class PaymentPortal(portal.CustomerPortal):
- """ This controller contains the foundations for online payments through the portal.
- It allows to complete a full payment flow without the need of going through a document-based
- flow made available by another module's controller.
- Such controllers should extend this one to gain access to the _create_transaction static method
- that implements the creation of a transaction before its processing, or to override specific
- routes and change their behavior globally (e.g. make the /pay route handle sale orders).
- The following routes are exposed:
- - `/payment/pay` allows for arbitrary payments.
- - `/my/payment_method` allows the user to create and delete tokens. It's its own `landing_route`
- - `/payment/transaction` is the `transaction_route` for the standard payment flow. It creates a
- draft transaction, and return the processing values necessary for the completion of the
- transaction.
- - `/payment/confirmation` is the `landing_route` for the standard payment flow. It displays the
- payment confirmation page to the user when the transaction is validated.
- """
- @http.route(
- '/payment/pay', type='http', methods=['GET'], auth='public', website=True, sitemap=False,
- )
- def payment_pay(
- self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None,
- provider_id=None, access_token=None, **kwargs
- ):
- """ Display the payment form with optional filtering of payment options.
- The filtering takes place on the basis of provided parameters, if any. If a parameter is
- incorrect or malformed, it is skipped to avoid preventing the user from making the payment.
- In addition to the desired filtering, a second one ensures that none of the following
- rules is broken:
- - Public users are not allowed to save their payment method as a token.
- - Payments made by public users should either *not* be made on behalf of a specific
- partner or have an access token validating the partner, amount and currency.
- We let access rights and security rules do their job for logged in users.
- :param str reference: The custom prefix to compute the full reference
- :param str amount: The amount to pay
- :param str currency_id: The desired currency, as a `res.currency` id
- :param str partner_id: The partner making the payment, as a `res.partner` id
- :param str company_id: The related company, as a `res.company` id
- :param str provider_id: The desired provider, as a `payment.provider` id
- :param str access_token: The access token used to authenticate the partner
- :param dict kwargs: Optional data passed to helper methods.
- :return: The rendered checkout form
- :rtype: str
- :raise: werkzeug.exceptions.NotFound if the access token is invalid
- """
- # Cast numeric parameters as int or float and void them if their str value is malformed
- currency_id, provider_id, partner_id, company_id = tuple(map(
- self._cast_as_int, (currency_id, provider_id, partner_id, company_id)
- ))
- amount = self._cast_as_float(amount)
- # Raise an HTTP 404 if a partner is provided with an invalid access token
- if partner_id:
- if not payment_utils.check_access_token(access_token, partner_id, amount, currency_id):
- raise werkzeug.exceptions.NotFound() # Don't leak information about ids.
- user_sudo = request.env.user
- logged_in = not user_sudo._is_public()
- # If the user is logged in, take their partner rather than the partner set in the params.
- # This is something that we want, since security rules are based on the partner, and created
- # tokens should not be assigned to the public user. This should have no impact on the
- # transaction itself besides making reconciliation possibly more difficult (e.g. The
- # transaction and invoice partners are different).
- partner_is_different = False
- if logged_in:
- partner_is_different = partner_id and partner_id != user_sudo.partner_id.id
- partner_sudo = user_sudo.partner_id
- else:
- partner_sudo = request.env['res.partner'].sudo().browse(partner_id).exists()
- if not partner_sudo:
- return request.redirect(
- # Escape special characters to avoid loosing original params when redirected
- f'/web/login?redirect={urllib.parse.quote(request.httprequest.full_path)}'
- )
- # Instantiate transaction values to their default if not set in parameters
- reference = reference or payment_utils.singularize_reference_prefix(prefix='tx')
- amount = amount or 0.0 # If the amount is invalid, set it to 0 to stop the payment flow
- company_id = company_id or partner_sudo.company_id.id or user_sudo.company_id.id
- company = request.env['res.company'].sudo().browse(company_id)
- currency_id = currency_id or company.currency_id.id
- # Make sure that the currency exists and is active
- currency = request.env['res.currency'].browse(currency_id).exists()
- if not currency or not currency.active:
- raise werkzeug.exceptions.NotFound() # The currency must exist and be active.
- # Select all providers and tokens that match the constraints
- providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
- company_id, partner_sudo.id, amount, currency_id=currency.id, **kwargs
- ) # In sudo mode to read the fields of providers and partner (if not logged in)
- if provider_id in providers_sudo.ids: # Only keep the desired provider if it's suitable
- providers_sudo = providers_sudo.browse(provider_id)
- payment_tokens = request.env['payment.token'].search(
- [('provider_id', 'in', providers_sudo.ids), ('partner_id', '=', partner_sudo.id)]
- ) if logged_in else request.env['payment.token']
- # Make sure that the partner's company matches the company passed as parameter.
- if not PaymentPortal._can_partner_pay_in_company(partner_sudo, company):
- providers_sudo = request.env['payment.provider'].sudo()
- payment_tokens = request.env['payment.token']
- # Compute the fees taken by providers supporting the feature
- fees_by_provider = {
- provider_sudo: provider_sudo._compute_fees(amount, currency, partner_sudo.country_id)
- for provider_sudo in providers_sudo.filtered('fees_active')
- }
- # Generate a new access token in case the partner id or the currency id was updated
- access_token = payment_utils.generate_access_token(partner_sudo.id, amount, currency.id)
- rendering_context = {
- 'providers': providers_sudo,
- 'tokens': payment_tokens,
- 'fees_by_provider': fees_by_provider,
- 'show_tokenize_input': self._compute_show_tokenize_input_mapping(
- providers_sudo, logged_in=logged_in, **kwargs
- ),
- 'reference_prefix': reference,
- 'amount': amount,
- 'currency': currency,
- 'partner_id': partner_sudo.id,
- 'access_token': access_token,
- 'transaction_route': '/payment/transaction',
- 'landing_route': '/payment/confirmation',
- 'res_company': company, # Display the correct logo in a multi-company environment
- 'partner_is_different': partner_is_different,
- **self._get_custom_rendering_context_values(**kwargs),
- }
- 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):
- """ 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
- show_tokenize_input_mapping[provider_sudo.id] = show_tokenize_input
- return show_tokenize_input_mapping
- def _get_payment_page_template_xmlid(self, **kwargs):
- return 'payment.pay'
- @http.route('/my/payment_method', type='http', methods=['GET'], auth='user', website=True)
- def payment_method(self, **kwargs):
- """ Display the form to manage payment methods.
- :param dict kwargs: Optional data. This parameter is not used here
- :return: The rendered manage form
- :rtype: str
- """
- partner_sudo = request.env.user.partner_id # env.user is always sudoed
- providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
- request.env.company.id,
- partner_sudo.id,
- 0., # There is no amount to pay with validation transactions.
- force_tokenization=True,
- is_validation=True,
- )
- # Get all partner's tokens for which providers are not disabled.
- tokens_sudo = request.env['payment.token'].sudo().search([
- ('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id]),
- ('provider_id.state', 'in', ['enabled', 'test']),
- ])
- access_token = payment_utils.generate_access_token(partner_sudo.id, None, None)
- rendering_context = {
- 'providers': providers_sudo,
- 'tokens': tokens_sudo,
- 'reference_prefix': payment_utils.singularize_reference_prefix(prefix='V'),
- 'partner_id': partner_sudo.id,
- 'access_token': access_token,
- 'transaction_route': '/payment/transaction',
- 'landing_route': '/my/payment_method',
- **self._get_custom_rendering_context_values(**kwargs),
- }
- return request.render('payment.payment_methods', rendering_context)
- def _get_custom_rendering_context_values(self, **kwargs):
- """ Return a dict of additional rendering context values.
- :param dict kwargs: Optional data. This parameter is not used here
- :return: The dict of additional rendering context values
- :rtype: dict
- """
- return {}
- @http.route('/payment/transaction', type='json', auth='public')
- def payment_transaction(self, amount, currency_id, partner_id, access_token, **kwargs):
- """ Create a draft transaction and return its processing values.
- :param float|None amount: The amount to pay in the given currency.
- None if in a payment method validation operation
- :param int|None currency_id: The currency of the transaction, as a `res.currency` id.
- None if in a payment method validation operation
- :param int partner_id: The partner making the payment, as a `res.partner` id
- :param str access_token: The access token used to authenticate the partner
- :param dict kwargs: Locally unused data passed to `_create_transaction`
- :return: The mandatory values for the processing of the transaction
- :rtype: dict
- :raise: ValidationError if the access token is invalid
- """
- # Check the access token against the transaction values
- amount = amount and float(amount) # Cast as float in case the JS stripped the '.0'
- if not payment_utils.check_access_token(access_token, partner_id, amount, currency_id):
- raise ValidationError(_("The access token is invalid."))
- kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
- tx_sudo = self._create_transaction(
- amount=amount, currency_id=currency_id, partner_id=partner_id, **kwargs
- )
- self._update_landing_route(tx_sudo, access_token) # Add the required parameters to the route
- return tx_sudo._get_processing_values()
- def _create_transaction(
- self, payment_option_id, reference_prefix, amount, currency_id, partner_id, flow,
- tokenization_requested, landing_route, is_validation=False,
- custom_create_values=None, **kwargs
- ):
- """ Create a draft transaction based on the payment context and return it.
- :param int payment_option_id: The payment option handling the transaction, as a
- `payment.provider` id or a `payment.token` id
- :param str reference_prefix: The custom prefix to compute the full reference
- :param float|None amount: The amount to pay in the given currency.
- None if in a payment method validation operation
- :param int|None currency_id: The currency of the transaction, as a `res.currency` id.
- None if in a payment method validation operation
- :param int partner_id: The partner making the payment, as a `res.partner` id
- :param str flow: The online payment flow of the transaction: 'redirect', 'direct' or 'token'
- :param bool tokenization_requested: Whether the user requested that a token is created
- :param str landing_route: The route the user is redirected to after the transaction
- :param bool is_validation: Whether the operation is a validation
- :param dict custom_create_values: Additional create values overwriting the default ones
- :param dict kwargs: Locally unused data passed to `_is_tokenization_required` and
- `_compute_reference`
- :return: The sudoed transaction that was created
- :rtype: recordset of `payment.transaction`
- :raise: UserError if the flow is invalid
- """
- # Prepare create values
- if flow in ['redirect', 'direct']: # Direct payment or payment with redirection
- provider_sudo = request.env['payment.provider'].sudo().browse(payment_option_id)
- token_id = None
- tokenize = bool(
- # Don't tokenize if the user tried to force it through the browser's developer tools
- provider_sudo.allow_tokenization
- # Token is only created if required by the flow or requested by the user
- and (provider_sudo._is_tokenization_required(**kwargs) or tokenization_requested)
- )
- elif flow == 'token': # Payment by token
- token_sudo = request.env['payment.token'].sudo().browse(payment_option_id)
- # Prevent from paying with a token that doesn't belong to the current partner (either
- # the current user's partner if logged in, or the partner on behalf of whom the payment
- # is being made).
- partner_sudo = request.env['res.partner'].sudo().browse(partner_id)
- if partner_sudo.commercial_partner_id != token_sudo.partner_id.commercial_partner_id:
- raise AccessError(_("You do not have access to this payment token."))
- provider_sudo = token_sudo.provider_id
- token_id = payment_option_id
- tokenize = False
- else:
- raise UserError(
- _("The payment should either be direct, with redirection, or made by a token.")
- )
- reference = request.env['payment.transaction']._compute_reference(
- provider_sudo.code,
- prefix=reference_prefix,
- **(custom_create_values or {}),
- **kwargs
- )
- if is_validation: # Providers determine the amount and currency in validation operations
- amount = provider_sudo._get_validation_amount()
- currency_id = provider_sudo._get_validation_currency().id
- # Create the transaction
- tx_sudo = request.env['payment.transaction'].sudo().create({
- 'provider_id': provider_sudo.id,
- 'reference': reference,
- 'amount': amount,
- 'currency_id': currency_id,
- 'partner_id': partner_id,
- 'token_id': token_id,
- 'operation': f'online_{flow}' if not is_validation else 'validation',
- 'tokenize': tokenize,
- 'landing_route': landing_route,
- **(custom_create_values or {}),
- }) # In sudo mode to allow writing on callback fields
- if flow == 'token':
- tx_sudo._send_payment_request() # Payments by token process transactions immediately
- else:
- tx_sudo._log_sent_message()
- # Monitor the transaction to make it available in the portal
- PaymentPostProcessing.monitor_transactions(tx_sudo)
- return tx_sudo
- @staticmethod
- def _update_landing_route(tx_sudo, access_token):
- """ Add the mandatory parameters to the route and recompute the access token if needed.
- The generic landing route requires the tx id and access token to be provided since there is
- no document to rely on. The access token is recomputed in case we are dealing with a
- validation transaction (provider-specific amount and currency).
- :param recordset tx_sudo: The transaction whose landing routes to update, as a
- `payment.transaction` record.
- :param str access_token: The access token used to authenticate the partner
- :return: None
- """
- if tx_sudo.operation == 'validation':
- access_token = payment_utils.generate_access_token(
- tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id
- )
- tx_sudo.landing_route = f'{tx_sudo.landing_route}' \
- f'?tx_id={tx_sudo.id}&access_token={access_token}'
- @http.route('/payment/confirmation', type='http', methods=['GET'], auth='public', website=True)
- def payment_confirm(self, tx_id, access_token, **kwargs):
- """ Display the payment confirmation page to the user.
- :param str tx_id: The transaction to confirm, as a `payment.transaction` id
- :param str access_token: The access token used to verify the user
- :param dict kwargs: Optional data. This parameter is not used here
- :raise: werkzeug.exceptions.NotFound if the access token is invalid
- """
- tx_id = self._cast_as_int(tx_id)
- if tx_id:
- tx_sudo = request.env['payment.transaction'].sudo().browse(tx_id)
- # Raise an HTTP 404 if the access token is invalid
- if not payment_utils.check_access_token(
- access_token, tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id
- ):
- raise werkzeug.exceptions.NotFound() # Don't leak information about ids.
- # Stop monitoring the transaction now that it reached a final state.
- PaymentPostProcessing.remove_transactions(tx_sudo)
- # Display the payment confirmation page to the user
- return request.render('payment.confirm', qcontext={'tx': tx_sudo})
- else:
- # Display the portal homepage to the user
- return request.redirect('/my/home')
- @http.route('/payment/archive_token', type='json', auth='user')
- def archive_token(self, token_id):
- """ Check that a user has write access on a token and archive the token if so.
- :param int token_id: The token to archive, as a `payment.token` id
- :return: None
- """
- partner_sudo = request.env.user.partner_id
- token_sudo = request.env['payment.token'].sudo().search([
- ('id', '=', token_id),
- # Check that the user owns the token before letting them archive anything
- ('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id])
- ])
- if token_sudo:
- token_sudo.active = False
- @staticmethod
- def _cast_as_int(str_value):
- """ Cast a string as an `int` and return it.
- If the conversion fails, `None` is returned instead.
- :param str str_value: The value to cast as an `int`
- :return: The casted value, possibly replaced by None if incompatible
- :rtype: int|None
- """
- try:
- return int(str_value)
- except (TypeError, ValueError, OverflowError):
- return None
- @staticmethod
- def _cast_as_float(str_value):
- """ Cast a string as a `float` and return it.
- If the conversion fails, `None` is returned instead.
- :param str str_value: The value to cast as a `float`
- :return: The casted value, possibly replaced by None if incompatible
- :rtype: float|None
- """
- try:
- return float(str_value)
- except (TypeError, ValueError, OverflowError):
- return None
- @staticmethod
- def _can_partner_pay_in_company(partner, document_company):
- """ Return whether the provided partner can pay in the provided company.
- The payment is allowed either if the partner's company is not set or if the companies match.
- :param recordset partner: The partner on behalf on which the payment is made, as a
- `res.partner` record.
- :param recordset document_company: The company of the document being paid, as a
- `res.company` record.
- :return: Whether the payment is allowed.
- :rtype: str
- """
- return not partner.company_id or partner.company_id == document_company
|