File: payment_provider.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (195 lines) | stat: -rw-r--r-- 8,215 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import json
import logging
import re
import requests

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError

from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_adyen import const

_logger = logging.getLogger(__name__)


class PaymentProvider(models.Model):
    _inherit = 'payment.provider'

    code = fields.Selection(
        selection_add=[('adyen', "Adyen")], ondelete={'adyen': 'set default'})
    adyen_merchant_account = fields.Char(
        string="Merchant Account",
        help="The code of the merchant account to use with this provider",
        required_if_provider='adyen', groups='base.group_system')
    adyen_api_key = fields.Char(
        string="API Key", help="The API key of the webservice user", required_if_provider='adyen',
        groups='base.group_system')
    adyen_client_key = fields.Char(
        string="Client Key", help="The client key of the webservice user",
        required_if_provider='adyen')
    adyen_hmac_key = fields.Char(
        string="HMAC Key", help="The HMAC key of the webhook", required_if_provider='adyen',
        groups='base.group_system')
    adyen_api_url_prefix = fields.Char(
        string="API URL Prefix",
        help="The base URL for the API endpoints",
        required_if_provider='adyen',
    )

    #=== CRUD METHODS ===#

    @api.model_create_multi
    def create(self, values_list):
        for values in values_list:
            self._adyen_extract_prefix_from_api_url(values)
        return super().create(values_list)

    def write(self, values):
        self._adyen_extract_prefix_from_api_url(values)
        return super().write(values)

    @api.model
    def _adyen_extract_prefix_from_api_url(self, values):
        """ Update the create or write values with the prefix extracted from the API URL.

        :param dict values: The create or write values.
        :return: None
        """
        if values.get('adyen_api_url_prefix'):  # Test if we're duplicating a provider.
            values['adyen_api_url_prefix'] = re.sub(
                r'(?:https://)?(\w+-\w+).*', r'\1', values['adyen_api_url_prefix']
            )

    #=== COMPUTE METHODS ===#

    def _compute_feature_support_fields(self):
        """ Override of `payment` to enable additional features. """
        super()._compute_feature_support_fields()
        self.filtered(lambda p: p.code == 'adyen').update({
            'support_manual_capture': 'partial',
            'support_refund': 'partial',
            'support_tokenization': True,
        })

    #=== BUSINESS METHODS - PAYMENT FLOW ===#

    def _adyen_make_request(self, endpoint, endpoint_param=None, payload=None, method='POST', idempotency_key=None):
        """ Make a request to Adyen API at the specified endpoint.

        Note: self.ensure_one()

        :param str endpoint: The endpoint to be reached by the request
        :param str endpoint_param: A variable required by some endpoints which are interpolated with
                                   it if provided. For example, the provider reference of the source
                                   transaction for the '/payments/{}/refunds' endpoint.
        :param dict payload: The payload of the request
        :param str method: The HTTP method of the request
        :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
        """

        def _build_url(prefix_, version_, endpoint_):
            """ Build an API URL by appending the version and endpoint to a base URL.

            The final URL follows this pattern: `<_base>/V<_version>/<_endpoint>`.

            :param str prefix_: The API URL prefix of the account.
            :param int version_: The version of the endpoint.
            :param str endpoint_: The endpoint of the URL.
            :return: The final URL.
            :rtype: str
            """
            prefix_ = prefix_.rstrip('/')  # Remove potential trailing slash
            endpoint_ = endpoint_.lstrip('/')  # Remove potential leading slash
            test_mode_ = self.state == 'test'
            prefix_ = f'{prefix_}.adyen' if test_mode_ else f'{prefix_}-checkout-live.adyenpayments'
            return f'https://{prefix_}.com/checkout/V{version_}/{endpoint_}'

        self.ensure_one()

        version = const.API_ENDPOINT_VERSIONS[endpoint]
        endpoint = endpoint if not endpoint_param else endpoint.format(endpoint_param)
        url = _build_url(self.adyen_api_url_prefix, version, endpoint)
        headers = {'X-API-Key': self.adyen_api_key}
        if method == 'POST' and idempotency_key:
            headers['idempotency-key'] = idempotency_key
        try:
            response = requests.request(method, url, json=payload, headers=headers, timeout=60)
            try:
                response.raise_for_status()
            except requests.exceptions.HTTPError:
                _logger.exception(
                    "invalid API request at %s with data %s: %s", url, payload, response.text
                )
                msg = response.json().get('message', '')
                raise ValidationError(
                    "Adyen: " + _("The communication with the API failed. Details: %s", msg)
                )
        except requests.exceptions.ConnectionError:
            _logger.exception("unable to reach endpoint at %s", url)
            raise ValidationError("Adyen: " + _("Could not establish the connection to the API."))
        return response.json()

    def _adyen_compute_shopper_reference(self, partner_id):
        """ Compute a unique reference of the partner for Adyen.

        This is used for the `shopperReference` field in communications with Adyen and stored in the
        `adyen_shopper_reference` field on `payment.token` if the payment method is tokenized.

        :param recordset partner_id: The partner making the transaction, as a `res.partner` id
        :return: The unique reference for the partner
        :rtype: str
        """
        return f'ODOO_PARTNER_{partner_id}'

    #=== BUSINESS METHODS - GETTERS ===#

    def _adyen_get_inline_form_values(self, pm_code, amount=None, currency=None):
        """ Return a serialized JSON of the required values to render the inline form.

        Note: `self.ensure_one()`

        :param str pm_code: The code of the payment method whose inline form to render.
        :param float amount: The transaction amount.
        :param res.currency currency: The transaction currency.
        :return: The JSON serial of the required values to render the inline form.
        :rtype: str
        """
        self.ensure_one()

        inline_form_values = {
            'client_key': self.adyen_client_key,
            'adyen_pm_code': const.PAYMENT_METHODS_MAPPING.get(pm_code, pm_code),
            'formatted_amount': self._adyen_get_formatted_amount(amount, currency),
        }
        return json.dumps(inline_form_values)

    def _adyen_get_formatted_amount(self, amount=None, currency=None):
        """ Return the amount in the format required by Adyen.

        The formatted amount is a dict with keys 'value' and 'currency'.

        :param float amount: The transaction amount.
        :param res.currency currency: The transaction currency.
        :return: The Adyen-formatted amount.
        :rtype: dict
        """
        currency_code = currency and currency.name
        converted_amount = amount and currency_code and payment_utils.to_minor_currency_units(
            amount, currency, const.CURRENCY_DECIMALS.get(currency_code)
        )
        return {
            'value': converted_amount,
            'currency': currency_code,
        }

    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 != 'adyen':
            return default_codes
        return const.DEFAULT_PAYMENT_METHOD_CODES