portal.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. import urllib.parse
  3. import werkzeug
  4. from odoo import _, http
  5. from odoo.exceptions import AccessError, UserError, ValidationError
  6. from odoo.http import request
  7. from odoo.addons.payment import utils as payment_utils
  8. from odoo.addons.payment.controllers.post_processing import PaymentPostProcessing
  9. from odoo.addons.portal.controllers import portal
  10. class PaymentPortal(portal.CustomerPortal):
  11. """ This controller contains the foundations for online payments through the portal.
  12. It allows to complete a full payment flow without the need of going through a document-based
  13. flow made available by another module's controller.
  14. Such controllers should extend this one to gain access to the _create_transaction static method
  15. that implements the creation of a transaction before its processing, or to override specific
  16. routes and change their behavior globally (e.g. make the /pay route handle sale orders).
  17. The following routes are exposed:
  18. - `/payment/pay` allows for arbitrary payments.
  19. - `/my/payment_method` allows the user to create and delete tokens. It's its own `landing_route`
  20. - `/payment/transaction` is the `transaction_route` for the standard payment flow. It creates a
  21. draft transaction, and return the processing values necessary for the completion of the
  22. transaction.
  23. - `/payment/confirmation` is the `landing_route` for the standard payment flow. It displays the
  24. payment confirmation page to the user when the transaction is validated.
  25. """
  26. @http.route(
  27. '/payment/pay', type='http', methods=['GET'], auth='public', website=True, sitemap=False,
  28. )
  29. def payment_pay(
  30. self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None,
  31. provider_id=None, access_token=None, **kwargs
  32. ):
  33. """ Display the payment form with optional filtering of payment options.
  34. The filtering takes place on the basis of provided parameters, if any. If a parameter is
  35. incorrect or malformed, it is skipped to avoid preventing the user from making the payment.
  36. In addition to the desired filtering, a second one ensures that none of the following
  37. rules is broken:
  38. - Public users are not allowed to save their payment method as a token.
  39. - Payments made by public users should either *not* be made on behalf of a specific
  40. partner or have an access token validating the partner, amount and currency.
  41. We let access rights and security rules do their job for logged in users.
  42. :param str reference: The custom prefix to compute the full reference
  43. :param str amount: The amount to pay
  44. :param str currency_id: The desired currency, as a `res.currency` id
  45. :param str partner_id: The partner making the payment, as a `res.partner` id
  46. :param str company_id: The related company, as a `res.company` id
  47. :param str provider_id: The desired provider, as a `payment.provider` id
  48. :param str access_token: The access token used to authenticate the partner
  49. :param dict kwargs: Optional data passed to helper methods.
  50. :return: The rendered checkout form
  51. :rtype: str
  52. :raise: werkzeug.exceptions.NotFound if the access token is invalid
  53. """
  54. # Cast numeric parameters as int or float and void them if their str value is malformed
  55. currency_id, provider_id, partner_id, company_id = tuple(map(
  56. self._cast_as_int, (currency_id, provider_id, partner_id, company_id)
  57. ))
  58. amount = self._cast_as_float(amount)
  59. # Raise an HTTP 404 if a partner is provided with an invalid access token
  60. if partner_id:
  61. if not payment_utils.check_access_token(access_token, partner_id, amount, currency_id):
  62. raise werkzeug.exceptions.NotFound() # Don't leak information about ids.
  63. user_sudo = request.env.user
  64. logged_in = not user_sudo._is_public()
  65. # If the user is logged in, take their partner rather than the partner set in the params.
  66. # This is something that we want, since security rules are based on the partner, and created
  67. # tokens should not be assigned to the public user. This should have no impact on the
  68. # transaction itself besides making reconciliation possibly more difficult (e.g. The
  69. # transaction and invoice partners are different).
  70. partner_is_different = False
  71. if logged_in:
  72. partner_is_different = partner_id and partner_id != user_sudo.partner_id.id
  73. partner_sudo = user_sudo.partner_id
  74. else:
  75. partner_sudo = request.env['res.partner'].sudo().browse(partner_id).exists()
  76. if not partner_sudo:
  77. return request.redirect(
  78. # Escape special characters to avoid loosing original params when redirected
  79. f'/web/login?redirect={urllib.parse.quote(request.httprequest.full_path)}'
  80. )
  81. # Instantiate transaction values to their default if not set in parameters
  82. reference = reference or payment_utils.singularize_reference_prefix(prefix='tx')
  83. amount = amount or 0.0 # If the amount is invalid, set it to 0 to stop the payment flow
  84. company_id = company_id or partner_sudo.company_id.id or user_sudo.company_id.id
  85. company = request.env['res.company'].sudo().browse(company_id)
  86. currency_id = currency_id or company.currency_id.id
  87. # Make sure that the currency exists and is active
  88. currency = request.env['res.currency'].browse(currency_id).exists()
  89. if not currency or not currency.active:
  90. raise werkzeug.exceptions.NotFound() # The currency must exist and be active.
  91. # Select all providers and tokens that match the constraints
  92. providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
  93. company_id, partner_sudo.id, amount, currency_id=currency.id, **kwargs
  94. ) # In sudo mode to read the fields of providers and partner (if not logged in)
  95. if provider_id in providers_sudo.ids: # Only keep the desired provider if it's suitable
  96. providers_sudo = providers_sudo.browse(provider_id)
  97. payment_tokens = request.env['payment.token'].search(
  98. [('provider_id', 'in', providers_sudo.ids), ('partner_id', '=', partner_sudo.id)]
  99. ) if logged_in else request.env['payment.token']
  100. # Make sure that the partner's company matches the company passed as parameter.
  101. if not PaymentPortal._can_partner_pay_in_company(partner_sudo, company):
  102. providers_sudo = request.env['payment.provider'].sudo()
  103. payment_tokens = request.env['payment.token']
  104. # Compute the fees taken by providers supporting the feature
  105. fees_by_provider = {
  106. provider_sudo: provider_sudo._compute_fees(amount, currency, partner_sudo.country_id)
  107. for provider_sudo in providers_sudo.filtered('fees_active')
  108. }
  109. # Generate a new access token in case the partner id or the currency id was updated
  110. access_token = payment_utils.generate_access_token(partner_sudo.id, amount, currency.id)
  111. rendering_context = {
  112. 'providers': providers_sudo,
  113. 'tokens': payment_tokens,
  114. 'fees_by_provider': fees_by_provider,
  115. 'show_tokenize_input': self._compute_show_tokenize_input_mapping(
  116. providers_sudo, logged_in=logged_in, **kwargs
  117. ),
  118. 'reference_prefix': reference,
  119. 'amount': amount,
  120. 'currency': currency,
  121. 'partner_id': partner_sudo.id,
  122. 'access_token': access_token,
  123. 'transaction_route': '/payment/transaction',
  124. 'landing_route': '/payment/confirmation',
  125. 'res_company': company, # Display the correct logo in a multi-company environment
  126. 'partner_is_different': partner_is_different,
  127. **self._get_custom_rendering_context_values(**kwargs),
  128. }
  129. return request.render(self._get_payment_page_template_xmlid(**kwargs), rendering_context)
  130. @staticmethod
  131. def _compute_show_tokenize_input_mapping(providers_sudo, logged_in=False, **kwargs):
  132. """ Determine for each provider whether the tokenization input should be shown or not.
  133. :param recordset providers_sudo: The providers for which to determine whether the
  134. tokenization input should be shown or not, as a sudoed
  135. `payment.provider` recordset.
  136. :param bool logged_in: Whether the user is logged in or not.
  137. :param dict kwargs: The optional data passed to the helper methods.
  138. :return: The mapping of the computed value for each provider id.
  139. :rtype: dict
  140. """
  141. show_tokenize_input_mapping = {}
  142. for provider_sudo in providers_sudo:
  143. show_tokenize_input = provider_sudo.allow_tokenization \
  144. and not provider_sudo._is_tokenization_required(**kwargs) \
  145. and logged_in
  146. show_tokenize_input_mapping[provider_sudo.id] = show_tokenize_input
  147. return show_tokenize_input_mapping
  148. def _get_payment_page_template_xmlid(self, **kwargs):
  149. return 'payment.pay'
  150. @http.route('/my/payment_method', type='http', methods=['GET'], auth='user', website=True)
  151. def payment_method(self, **kwargs):
  152. """ Display the form to manage payment methods.
  153. :param dict kwargs: Optional data. This parameter is not used here
  154. :return: The rendered manage form
  155. :rtype: str
  156. """
  157. partner_sudo = request.env.user.partner_id # env.user is always sudoed
  158. providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
  159. request.env.company.id,
  160. partner_sudo.id,
  161. 0., # There is no amount to pay with validation transactions.
  162. force_tokenization=True,
  163. is_validation=True,
  164. )
  165. # Get all partner's tokens for which providers are not disabled.
  166. tokens_sudo = request.env['payment.token'].sudo().search([
  167. ('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id]),
  168. ('provider_id.state', 'in', ['enabled', 'test']),
  169. ])
  170. access_token = payment_utils.generate_access_token(partner_sudo.id, None, None)
  171. rendering_context = {
  172. 'providers': providers_sudo,
  173. 'tokens': tokens_sudo,
  174. 'reference_prefix': payment_utils.singularize_reference_prefix(prefix='V'),
  175. 'partner_id': partner_sudo.id,
  176. 'access_token': access_token,
  177. 'transaction_route': '/payment/transaction',
  178. 'landing_route': '/my/payment_method',
  179. **self._get_custom_rendering_context_values(**kwargs),
  180. }
  181. return request.render('payment.payment_methods', rendering_context)
  182. def _get_custom_rendering_context_values(self, **kwargs):
  183. """ Return a dict of additional rendering context values.
  184. :param dict kwargs: Optional data. This parameter is not used here
  185. :return: The dict of additional rendering context values
  186. :rtype: dict
  187. """
  188. return {}
  189. @http.route('/payment/transaction', type='json', auth='public')
  190. def payment_transaction(self, amount, currency_id, partner_id, access_token, **kwargs):
  191. """ Create a draft transaction and return its processing values.
  192. :param float|None amount: The amount to pay in the given currency.
  193. None if in a payment method validation operation
  194. :param int|None currency_id: The currency of the transaction, as a `res.currency` id.
  195. None if in a payment method validation operation
  196. :param int partner_id: The partner making the payment, as a `res.partner` id
  197. :param str access_token: The access token used to authenticate the partner
  198. :param dict kwargs: Locally unused data passed to `_create_transaction`
  199. :return: The mandatory values for the processing of the transaction
  200. :rtype: dict
  201. :raise: ValidationError if the access token is invalid
  202. """
  203. # Check the access token against the transaction values
  204. amount = amount and float(amount) # Cast as float in case the JS stripped the '.0'
  205. if not payment_utils.check_access_token(access_token, partner_id, amount, currency_id):
  206. raise ValidationError(_("The access token is invalid."))
  207. kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
  208. tx_sudo = self._create_transaction(
  209. amount=amount, currency_id=currency_id, partner_id=partner_id, **kwargs
  210. )
  211. self._update_landing_route(tx_sudo, access_token) # Add the required parameters to the route
  212. return tx_sudo._get_processing_values()
  213. def _create_transaction(
  214. self, payment_option_id, reference_prefix, amount, currency_id, partner_id, flow,
  215. tokenization_requested, landing_route, is_validation=False,
  216. custom_create_values=None, **kwargs
  217. ):
  218. """ Create a draft transaction based on the payment context and return it.
  219. :param int payment_option_id: The payment option handling the transaction, as a
  220. `payment.provider` id or a `payment.token` id
  221. :param str reference_prefix: The custom prefix to compute the full reference
  222. :param float|None amount: The amount to pay in the given currency.
  223. None if in a payment method validation operation
  224. :param int|None currency_id: The currency of the transaction, as a `res.currency` id.
  225. None if in a payment method validation operation
  226. :param int partner_id: The partner making the payment, as a `res.partner` id
  227. :param str flow: The online payment flow of the transaction: 'redirect', 'direct' or 'token'
  228. :param bool tokenization_requested: Whether the user requested that a token is created
  229. :param str landing_route: The route the user is redirected to after the transaction
  230. :param bool is_validation: Whether the operation is a validation
  231. :param dict custom_create_values: Additional create values overwriting the default ones
  232. :param dict kwargs: Locally unused data passed to `_is_tokenization_required` and
  233. `_compute_reference`
  234. :return: The sudoed transaction that was created
  235. :rtype: recordset of `payment.transaction`
  236. :raise: UserError if the flow is invalid
  237. """
  238. # Prepare create values
  239. if flow in ['redirect', 'direct']: # Direct payment or payment with redirection
  240. provider_sudo = request.env['payment.provider'].sudo().browse(payment_option_id)
  241. token_id = None
  242. tokenize = bool(
  243. # Don't tokenize if the user tried to force it through the browser's developer tools
  244. provider_sudo.allow_tokenization
  245. # Token is only created if required by the flow or requested by the user
  246. and (provider_sudo._is_tokenization_required(**kwargs) or tokenization_requested)
  247. )
  248. elif flow == 'token': # Payment by token
  249. token_sudo = request.env['payment.token'].sudo().browse(payment_option_id)
  250. # Prevent from paying with a token that doesn't belong to the current partner (either
  251. # the current user's partner if logged in, or the partner on behalf of whom the payment
  252. # is being made).
  253. partner_sudo = request.env['res.partner'].sudo().browse(partner_id)
  254. if partner_sudo.commercial_partner_id != token_sudo.partner_id.commercial_partner_id:
  255. raise AccessError(_("You do not have access to this payment token."))
  256. provider_sudo = token_sudo.provider_id
  257. token_id = payment_option_id
  258. tokenize = False
  259. else:
  260. raise UserError(
  261. _("The payment should either be direct, with redirection, or made by a token.")
  262. )
  263. reference = request.env['payment.transaction']._compute_reference(
  264. provider_sudo.code,
  265. prefix=reference_prefix,
  266. **(custom_create_values or {}),
  267. **kwargs
  268. )
  269. if is_validation: # Providers determine the amount and currency in validation operations
  270. amount = provider_sudo._get_validation_amount()
  271. currency_id = provider_sudo._get_validation_currency().id
  272. # Create the transaction
  273. tx_sudo = request.env['payment.transaction'].sudo().create({
  274. 'provider_id': provider_sudo.id,
  275. 'reference': reference,
  276. 'amount': amount,
  277. 'currency_id': currency_id,
  278. 'partner_id': partner_id,
  279. 'token_id': token_id,
  280. 'operation': f'online_{flow}' if not is_validation else 'validation',
  281. 'tokenize': tokenize,
  282. 'landing_route': landing_route,
  283. **(custom_create_values or {}),
  284. }) # In sudo mode to allow writing on callback fields
  285. if flow == 'token':
  286. tx_sudo._send_payment_request() # Payments by token process transactions immediately
  287. else:
  288. tx_sudo._log_sent_message()
  289. # Monitor the transaction to make it available in the portal
  290. PaymentPostProcessing.monitor_transactions(tx_sudo)
  291. return tx_sudo
  292. @staticmethod
  293. def _update_landing_route(tx_sudo, access_token):
  294. """ Add the mandatory parameters to the route and recompute the access token if needed.
  295. The generic landing route requires the tx id and access token to be provided since there is
  296. no document to rely on. The access token is recomputed in case we are dealing with a
  297. validation transaction (provider-specific amount and currency).
  298. :param recordset tx_sudo: The transaction whose landing routes to update, as a
  299. `payment.transaction` record.
  300. :param str access_token: The access token used to authenticate the partner
  301. :return: None
  302. """
  303. if tx_sudo.operation == 'validation':
  304. access_token = payment_utils.generate_access_token(
  305. tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id
  306. )
  307. tx_sudo.landing_route = f'{tx_sudo.landing_route}' \
  308. f'?tx_id={tx_sudo.id}&access_token={access_token}'
  309. @http.route('/payment/confirmation', type='http', methods=['GET'], auth='public', website=True)
  310. def payment_confirm(self, tx_id, access_token, **kwargs):
  311. """ Display the payment confirmation page to the user.
  312. :param str tx_id: The transaction to confirm, as a `payment.transaction` id
  313. :param str access_token: The access token used to verify the user
  314. :param dict kwargs: Optional data. This parameter is not used here
  315. :raise: werkzeug.exceptions.NotFound if the access token is invalid
  316. """
  317. tx_id = self._cast_as_int(tx_id)
  318. if tx_id:
  319. tx_sudo = request.env['payment.transaction'].sudo().browse(tx_id)
  320. # Raise an HTTP 404 if the access token is invalid
  321. if not payment_utils.check_access_token(
  322. access_token, tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id
  323. ):
  324. raise werkzeug.exceptions.NotFound() # Don't leak information about ids.
  325. # Stop monitoring the transaction now that it reached a final state.
  326. PaymentPostProcessing.remove_transactions(tx_sudo)
  327. # Display the payment confirmation page to the user
  328. return request.render('payment.confirm', qcontext={'tx': tx_sudo})
  329. else:
  330. # Display the portal homepage to the user
  331. return request.redirect('/my/home')
  332. @http.route('/payment/archive_token', type='json', auth='user')
  333. def archive_token(self, token_id):
  334. """ Check that a user has write access on a token and archive the token if so.
  335. :param int token_id: The token to archive, as a `payment.token` id
  336. :return: None
  337. """
  338. partner_sudo = request.env.user.partner_id
  339. token_sudo = request.env['payment.token'].sudo().search([
  340. ('id', '=', token_id),
  341. # Check that the user owns the token before letting them archive anything
  342. ('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id])
  343. ])
  344. if token_sudo:
  345. token_sudo.active = False
  346. @staticmethod
  347. def _cast_as_int(str_value):
  348. """ Cast a string as an `int` and return it.
  349. If the conversion fails, `None` is returned instead.
  350. :param str str_value: The value to cast as an `int`
  351. :return: The casted value, possibly replaced by None if incompatible
  352. :rtype: int|None
  353. """
  354. try:
  355. return int(str_value)
  356. except (TypeError, ValueError, OverflowError):
  357. return None
  358. @staticmethod
  359. def _cast_as_float(str_value):
  360. """ Cast a string as a `float` and return it.
  361. If the conversion fails, `None` is returned instead.
  362. :param str str_value: The value to cast as a `float`
  363. :return: The casted value, possibly replaced by None if incompatible
  364. :rtype: float|None
  365. """
  366. try:
  367. return float(str_value)
  368. except (TypeError, ValueError, OverflowError):
  369. return None
  370. @staticmethod
  371. def _can_partner_pay_in_company(partner, document_company):
  372. """ Return whether the provided partner can pay in the provided company.
  373. The payment is allowed either if the partner's company is not set or if the companies match.
  374. :param recordset partner: The partner on behalf on which the payment is made, as a
  375. `res.partner` record.
  376. :param recordset document_company: The company of the document being paid, as a
  377. `res.company` record.
  378. :return: Whether the payment is allowed.
  379. :rtype: str
  380. """
  381. return not partner.company_id or partner.company_id == document_company