191 lines
7.4 KiB
Python
191 lines
7.4 KiB
Python
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
import json
|
||
|
import logging
|
||
|
import pprint
|
||
|
import requests
|
||
|
|
||
|
from datetime import timedelta
|
||
|
from werkzeug import urls
|
||
|
|
||
|
from odoo import _, fields, models
|
||
|
from odoo.exceptions import UserError, ValidationError
|
||
|
|
||
|
from odoo.addons.payment_paypal import const
|
||
|
from odoo.addons.payment_paypal.controllers.main import PaypalController
|
||
|
|
||
|
|
||
|
_logger = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
class PaymentProvider(models.Model):
|
||
|
_inherit = 'payment.provider'
|
||
|
|
||
|
code = fields.Selection(
|
||
|
selection_add=[('paypal', "PayPal")], ondelete={'paypal': 'set default'}
|
||
|
)
|
||
|
paypal_email_account = fields.Char(
|
||
|
string="Email",
|
||
|
help="The public business email solely used to identify the account with PayPal",
|
||
|
required_if_provider='paypal',
|
||
|
default=lambda self: self.env.company.email,
|
||
|
)
|
||
|
paypal_client_id = fields.Char(string="PayPal Client ID", required_if_provider='paypal')
|
||
|
paypal_client_secret = fields.Char(string="PayPal Client Secret", groups='base.group_system')
|
||
|
paypal_access_token = fields.Char(
|
||
|
string="PayPal Access Token",
|
||
|
help="The short-lived token used to access Paypal APIs",
|
||
|
groups='base.group_system',
|
||
|
)
|
||
|
paypal_access_token_expiry = fields.Datetime(
|
||
|
string="PayPal Access Token Expiry",
|
||
|
help="The moment at which the access token becomes invalid.",
|
||
|
default='1970-01-01',
|
||
|
groups='base.group_system',
|
||
|
)
|
||
|
paypal_webhook_id = fields.Char(string="PayPal Webhook ID")
|
||
|
|
||
|
# === ACTION METHODS === #
|
||
|
|
||
|
def action_paypal_create_webhook(self):
|
||
|
""" Create a new webhook.
|
||
|
|
||
|
Note: This action only works for instances using a public URL.
|
||
|
|
||
|
:return: None
|
||
|
:raise UserError: If the base URL is not in HTTPS.
|
||
|
"""
|
||
|
base_url = self.get_base_url()
|
||
|
if 'localhost' in base_url:
|
||
|
raise UserError(
|
||
|
"PayPal: " + _("You must have an HTTPS connection to generate a webhook.")
|
||
|
)
|
||
|
data = {
|
||
|
'url': urls.url_join(base_url, PaypalController._webhook_url),
|
||
|
'event_types': [{'name': event_type} for event_type in const.HANDLED_WEBHOOK_EVENTS]
|
||
|
}
|
||
|
webhook_data = self._paypal_make_request('/v1/notifications/webhooks', json_payload=data)
|
||
|
self.paypal_webhook_id = webhook_data.get('id')
|
||
|
|
||
|
#=== BUSINESS METHODS ===#
|
||
|
|
||
|
def _paypal_make_request(
|
||
|
self, endpoint, data=None, json_payload=None, auth=None, is_refresh_token_request=False,
|
||
|
idempotency_key=None,
|
||
|
):
|
||
|
""" Make a request to Paypal API at the specified endpoint.
|
||
|
|
||
|
Note: self.ensure_one()
|
||
|
|
||
|
:param str endpoint: The endpoint to be reached by the request.
|
||
|
:param dict data: The string payload of the request.
|
||
|
:param dict json_payload: The JSON-formatted payload of the request.
|
||
|
:param tuple auth: The authentication data.
|
||
|
:param bool is_refresh_token_request: Whether the request is for refreshing the access
|
||
|
token.
|
||
|
:param str idempotency_key: The idempotency key to pass in the request.
|
||
|
:return: The JSON-formatted content of the response.
|
||
|
:rtype: dict
|
||
|
:raise ValidationError: If an HTTP error occurs.
|
||
|
"""
|
||
|
url = self._paypal_get_api_url() + endpoint
|
||
|
headers = {'Content-Type': 'application/json'} # PayPal always wants JSON content-type.
|
||
|
if idempotency_key:
|
||
|
headers['PayPal-Request-Id'] = idempotency_key
|
||
|
if not is_refresh_token_request:
|
||
|
headers['Authorization'] = f'Bearer {self._paypal_fetch_access_token()}'
|
||
|
try:
|
||
|
response = requests.post(
|
||
|
url, headers=headers, data=data, json=json_payload, auth=auth, timeout=10
|
||
|
)
|
||
|
try:
|
||
|
response.raise_for_status()
|
||
|
except requests.exceptions.HTTPError:
|
||
|
payload = data or json_payload
|
||
|
# PayPal errors https://developer.paypal.com/api/rest/reference/orders/v2/errors/
|
||
|
_logger.exception(
|
||
|
"Invalid API request at %s with data:\n%s", url, pprint.pformat(payload)
|
||
|
)
|
||
|
msg = response.json().get('message', '')
|
||
|
raise ValidationError(
|
||
|
"PayPal: " + _("The communication with the API failed. Details: %s", msg)
|
||
|
)
|
||
|
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
||
|
_logger.exception("Unable to reach endpoint at %s", url)
|
||
|
raise ValidationError("PayPal: " + _("Could not establish the connection to the API."))
|
||
|
return response.json()
|
||
|
|
||
|
def _paypal_fetch_access_token(self):
|
||
|
""" Generate a new access token if it's expired, otherwise return the existing access token.
|
||
|
|
||
|
:return: A valid access token.
|
||
|
:rtype: str
|
||
|
:raise ValidationError: If the access token can not be fetched.
|
||
|
"""
|
||
|
if fields.Datetime.now() > self.paypal_access_token_expiry - timedelta(minutes=5):
|
||
|
response_content = self._paypal_make_request(
|
||
|
'/v1/oauth2/token',
|
||
|
data={'grant_type': 'client_credentials'},
|
||
|
auth=(self.paypal_client_id, self.paypal_client_secret),
|
||
|
is_refresh_token_request=True,
|
||
|
)
|
||
|
access_token = response_content['access_token']
|
||
|
if not access_token:
|
||
|
raise ValidationError("PayPal: " + _("Could not generate a new access token."))
|
||
|
self.write({
|
||
|
'paypal_access_token': access_token,
|
||
|
'paypal_access_token_expiry': fields.Datetime.now() + timedelta(
|
||
|
seconds=response_content['expires_in']
|
||
|
),
|
||
|
})
|
||
|
return self.paypal_access_token
|
||
|
|
||
|
# === BUSINESS METHODS - GETTERS === #
|
||
|
|
||
|
def _get_supported_currencies(self):
|
||
|
""" Override of `payment` to return the supported currencies. """
|
||
|
supported_currencies = super()._get_supported_currencies()
|
||
|
if self.code == 'paypal':
|
||
|
supported_currencies = supported_currencies.filtered(
|
||
|
lambda c: c.name in const.SUPPORTED_CURRENCIES
|
||
|
)
|
||
|
return supported_currencies
|
||
|
|
||
|
def _paypal_get_api_url(self):
|
||
|
""" Return the API URL according to the provider state.
|
||
|
|
||
|
Note: self.ensure_one()
|
||
|
|
||
|
:return: The API URL
|
||
|
:rtype: str
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
|
||
|
if self.state == 'enabled':
|
||
|
return 'https://api-m.paypal.com'
|
||
|
else:
|
||
|
return 'https://api-m.sandbox.paypal.com'
|
||
|
|
||
|
def _get_default_payment_method_codes(self):
|
||
|
""" Override of `payment` to return the default payment method codes. """
|
||
|
default_codes = super()._get_default_payment_method_codes()
|
||
|
if self.code != 'paypal':
|
||
|
return default_codes
|
||
|
return const.DEFAULT_PAYMENT_METHOD_CODES
|
||
|
|
||
|
def _paypal_get_inline_form_values(self, currency=None):
|
||
|
""" Return a serialized JSON of the required values to render the inline form.
|
||
|
|
||
|
Note: `self.ensure_one()`
|
||
|
|
||
|
:param res.currency currency: The transaction currency.
|
||
|
:return: The JSON serial of the required values to render the inline form.
|
||
|
:rtype: str
|
||
|
"""
|
||
|
inline_form_values = {
|
||
|
'provider_id': self.id,
|
||
|
'client_id': self.paypal_client_id,
|
||
|
'currency_code': currency and currency.name,
|
||
|
}
|
||
|
return json.dumps(inline_form_values)
|