payment_provider.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  2. import logging
  3. import re
  4. import requests
  5. from odoo import _, api, fields, models
  6. from odoo.exceptions import ValidationError
  7. from odoo.addons.payment_adyen.const import API_ENDPOINT_VERSIONS
  8. _logger = logging.getLogger(__name__)
  9. class PaymentProvider(models.Model):
  10. _inherit = 'payment.provider'
  11. code = fields.Selection(
  12. selection_add=[('adyen', "Adyen")], ondelete={'adyen': 'set default'})
  13. adyen_merchant_account = fields.Char(
  14. string="Merchant Account",
  15. help="The code of the merchant account to use with this provider",
  16. required_if_provider='adyen', groups='base.group_system')
  17. adyen_api_key = fields.Char(
  18. string="API Key", help="The API key of the webservice user", required_if_provider='adyen',
  19. groups='base.group_system')
  20. adyen_client_key = fields.Char(
  21. string="Client Key", help="The client key of the webservice user",
  22. required_if_provider='adyen')
  23. adyen_hmac_key = fields.Char(
  24. string="HMAC Key", help="The HMAC key of the webhook", required_if_provider='adyen',
  25. groups='base.group_system')
  26. adyen_checkout_api_url = fields.Char(
  27. string="Checkout API URL", help="The base URL for the Checkout API endpoints",
  28. required_if_provider='adyen')
  29. adyen_recurring_api_url = fields.Char(
  30. string="Recurring API URL", help="The base URL for the Recurring API endpoints",
  31. required_if_provider='adyen')
  32. #=== CRUD METHODS ===#
  33. @api.model_create_multi
  34. def create(self, values_list):
  35. for values in values_list:
  36. self._adyen_trim_api_urls(values)
  37. return super().create(values_list)
  38. def write(self, values):
  39. self._adyen_trim_api_urls(values)
  40. return super().write(values)
  41. @api.model
  42. def _adyen_trim_api_urls(self, values):
  43. """ Remove the version and the endpoint from the url of Adyen API fields.
  44. :param dict values: The create or write values
  45. :return: None
  46. """
  47. for field_name in ('adyen_checkout_api_url', 'adyen_recurring_api_url'):
  48. if values.get(field_name): # Test the value in case we're duplicating a provider
  49. values[field_name] = re.sub(r'[vV]\d+(/.*)?', '', values[field_name])
  50. #=== COMPUTE METHODS ===#
  51. def _compute_feature_support_fields(self):
  52. """ Override of `payment` to enable additional features. """
  53. super()._compute_feature_support_fields()
  54. self.filtered(lambda p: p.code == 'adyen').update({
  55. 'support_manual_capture': True,
  56. 'support_refund': 'partial',
  57. 'support_tokenization': True,
  58. })
  59. #=== BUSINESS METHODS ===#
  60. def _adyen_make_request(
  61. self, url_field_name, endpoint, endpoint_param=None, payload=None, method='POST'
  62. ):
  63. """ Make a request to Adyen API at the specified endpoint.
  64. Note: self.ensure_one()
  65. :param str url_field_name: The name of the field holding the base URL for the request
  66. :param str endpoint: The endpoint to be reached by the request
  67. :param str endpoint_param: A variable required by some endpoints which are interpolated with
  68. it if provided. For example, the provider reference of the source
  69. transaction for the '/payments/{}/refunds' endpoint.
  70. :param dict payload: The payload of the request
  71. :param str method: The HTTP method of the request
  72. :return: The JSON-formatted content of the response
  73. :rtype: dict
  74. :raise: ValidationError if an HTTP error occurs
  75. """
  76. def _build_url(_base_url, _version, _endpoint):
  77. """ Build an API URL by appending the version and endpoint to a base URL.
  78. The final URL follows this pattern: `<_base>/V<_version>/<_endpoint>`.
  79. :param str _base_url: The base of the url prefixed with `https://`
  80. :param int _version: The version of the endpoint
  81. :param str _endpoint: The endpoint of the URL.
  82. :return: The final URL
  83. :rtype: str
  84. """
  85. _base = _base_url.rstrip('/') # Remove potential trailing slash
  86. _endpoint = _endpoint.lstrip('/') # Remove potential leading slash
  87. return f'{_base}/V{_version}/{_endpoint}'
  88. self.ensure_one()
  89. base_url = self[url_field_name] # Restrict request URL to the stored API URL fields
  90. version = API_ENDPOINT_VERSIONS[endpoint]
  91. endpoint = endpoint if not endpoint_param else endpoint.format(endpoint_param)
  92. url = _build_url(base_url, version, endpoint)
  93. headers = {'X-API-Key': self.adyen_api_key}
  94. try:
  95. response = requests.request(method, url, json=payload, headers=headers, timeout=60)
  96. response.raise_for_status()
  97. except requests.exceptions.ConnectionError:
  98. _logger.exception("unable to reach endpoint at %s", url)
  99. raise ValidationError("Adyen: " + _("Could not establish the connection to the API."))
  100. except requests.exceptions.HTTPError as error:
  101. _logger.exception(
  102. "invalid API request at %s with data %s: %s", url, payload, error.response.text
  103. )
  104. raise ValidationError("Adyen: " + _("The communication with the API failed."))
  105. return response.json()
  106. def _adyen_compute_shopper_reference(self, partner_id):
  107. """ Compute a unique reference of the partner for Adyen.
  108. This is used for the `shopperReference` field in communications with Adyen and stored in the
  109. `adyen_shopper_reference` field on `payment.token` if the payment method is tokenized.
  110. :param recordset partner_id: The partner making the transaction, as a `res.partner` id
  111. :return: The unique reference for the partner
  112. :rtype: str
  113. """
  114. return f'ODOO_PARTNER_{partner_id}'