File: payment_portal.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 (295 lines) | stat: -rw-r--r-- 14,275 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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from werkzeug.urls import url_encode

from odoo import _, http, tools
from odoo.http import request
from odoo.exceptions import AccessError, ValidationError, UserError
from odoo.addons.payment.controllers import portal as payment_portal


class PaymentPortal(payment_portal.PaymentPortal):

    def _check_order_access(self, pos_order_id, access_token):
        try:
            order_sudo = self._document_check_access(
                'pos.order', pos_order_id, access_token)
        except:
            raise AccessError(
                _("The provided order or access token is invalid."))

        if order_sudo.state == "cancel":
            raise ValidationError(_("The order has been cancelled."))
        return order_sudo

    @staticmethod
    def _ensure_session_open(pos_order_sudo):
        if pos_order_sudo.session_id.state != 'opened':
            raise AccessError(_("The POS session is not opened."))

    def _get_partner_sudo(self, user_sudo):
        return user_sudo.partner_id

    def _redirect_login(self):
        return request.redirect('/web/login?' + url_encode({'redirect': request.httprequest.full_path}))

    @staticmethod
    def _get_amount_to_pay(order_to_pay_sudo):
        if order_to_pay_sudo.state in ('paid', 'done', 'invoiced'):
            return 0.0
        amount = order_to_pay_sudo._get_checked_next_online_payment_amount()
        if amount and PaymentPortal._is_valid_amount(amount, order_to_pay_sudo.currency_id):
            return amount
        else:
            return order_to_pay_sudo.get_amount_unpaid()

    @staticmethod
    def _is_valid_amount(amount, currency):
        return isinstance(amount, float) and tools.float_compare(amount, 0.0, precision_rounding=currency.rounding) > 0

    def _get_allowed_providers_sudo(self, pos_order_sudo, partner_id, amount_to_pay):
        payment_method = pos_order_sudo.online_payment_method_id
        if not payment_method:
            raise UserError(_("There is no online payment method configured for this Point of Sale order."))
        compatible_providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
            pos_order_sudo.company_id.id, partner_id, amount_to_pay, currency_id=pos_order_sudo.currency_id.id
        )  # In sudo mode to read the fields of providers and partner (if logged out).
        # Return the payment providers configured in the pos.payment.method that are compatible for the payment API
        return compatible_providers_sudo & payment_method._get_online_payment_providers(pos_order_sudo.config_id.id, error_if_invalid=False)

    @staticmethod
    def _new_url_params(access_token, exit_route=None):
        url_params = {
            'access_token': access_token,
        }
        if exit_route:
            url_params['exit_route'] = exit_route
        return url_params

    @staticmethod
    def _get_pay_route(pos_order_id, access_token, exit_route=None):
        return f'/pos/pay/{pos_order_id}?' + url_encode(PaymentPortal._new_url_params(access_token, exit_route))

    @staticmethod
    def _get_landing_route(pos_order_id, access_token, exit_route=None, tx_id=None):
        url_params = PaymentPortal._new_url_params(access_token, exit_route)
        if tx_id:
            url_params['tx_id'] = tx_id
        return f'/pos/pay/confirmation/{pos_order_id}?' + url_encode(url_params)

    @http.route('/pos/pay/<int:pos_order_id>', type='http', methods=['GET'], auth='public', website=True, sitemap=False)
    def pos_order_pay(self, pos_order_id, access_token=None, exit_route=None):
        """ Behaves like payment.PaymentPortal.payment_pay but for POS online payment.

        :param int pos_order_id: The POS order to pay, as a `pos.order` id
        :param str access_token: The access token used to verify the user
        :param str exit_route: The URL to open to leave the POS online payment flow

        :return: The rendered payment form
        :rtype: str
        :raise: AccessError if the provided order or access token is invalid
        :raise: ValidationError if data on the server prevents the payment
        """
        pos_order_sudo = self._check_order_access(pos_order_id, access_token)
        self._ensure_session_open(pos_order_sudo)

        user_sudo = request.env.user
        if not pos_order_sudo.partner_id:
            user_sudo = request.env.ref('base.public_user')
        logged_in = not user_sudo._is_public()
        partner_sudo = pos_order_sudo.partner_id or self._get_partner_sudo(user_sudo)
        if not partner_sudo:
            return self._redirect_login()

        kwargs = {
            'pos_order_id': pos_order_sudo.id,
        }
        rendering_context = {
            **kwargs,
            'exit_route': exit_route,
            'reference_prefix': request.env['payment.transaction'].sudo()._compute_reference_prefix(provider_code=None, separator='-', **kwargs),
            'partner_id': partner_sudo.id,
            'access_token': access_token,
            'transaction_route': f'/pos/pay/transaction/{pos_order_sudo.id}?' + url_encode(PaymentPortal._new_url_params(access_token, exit_route)),
            'landing_route': self._get_landing_route(pos_order_sudo.id, access_token, exit_route=exit_route),
            **self._get_extra_payment_form_values(**kwargs),
        }

        currency_id = pos_order_sudo.currency_id

        if not currency_id.active:
            rendering_context['currency'] = False
            return self._render_pay(rendering_context)
        rendering_context['currency'] = currency_id

        amount_to_pay = self._get_amount_to_pay(pos_order_sudo)
        if not self._is_valid_amount(amount_to_pay, currency_id):
            rendering_context['amount'] = False
            return self._render_pay(rendering_context)
        rendering_context['amount'] = amount_to_pay

        # Select all the payment methods and tokens that match the payment context.
        providers_sudo = self._get_allowed_providers_sudo(pos_order_sudo, partner_sudo.id, amount_to_pay)
        payment_methods_sudo = request.env['payment.method'].sudo()._get_compatible_payment_methods(
            providers_sudo.ids,
            partner_sudo.id,
            currency_id=currency_id.id,
        )  # In sudo mode to read the fields of providers.
        if logged_in:
            tokens_sudo = request.env['payment.token'].sudo()._get_available_tokens(
                providers_sudo.ids, partner_sudo.id
            )  # In sudo mode to be able to read the fields of providers.
            show_tokenize_input_mapping = self._compute_show_tokenize_input_mapping(
                providers_sudo, **kwargs)
        else:
            tokens_sudo = request.env['payment.token']
            show_tokenize_input_mapping = dict.fromkeys(providers_sudo.ids, False)

        rendering_context.update({
            'providers_sudo': providers_sudo,
            'payment_methods_sudo': payment_methods_sudo,
            'tokens_sudo': tokens_sudo,
            'show_tokenize_input_mapping': show_tokenize_input_mapping,
            **self._get_extra_payment_form_values(**kwargs),
        })
        return self._render_pay(rendering_context)

    def _render_pay(self, rendering_context):
        return request.render('pos_online_payment.pay', rendering_context)

    @http.route('/pos/pay/transaction/<int:pos_order_id>', type='json', auth='public', website=True, sitemap=False)
    def pos_order_pay_transaction(self, pos_order_id, access_token=None, **kwargs):
        """ Behaves like payment.PaymentPortal.payment_transaction but for POS online payment.

        :param int pos_order_id: The POS order to pay, as a `pos.order` id
        :param str access_token: The access token used to verify the user
        :param str exit_route: The URL to open to leave the POS online payment flow
        :param dict kwargs: Data from payment module

        :return: The mandatory values for the processing of the transaction
        :rtype: dict
        :raise: AccessError if the provided order or access token is invalid
        :raise: ValidationError if data on the server prevents the payment
        :raise: UserError if data provided by the user is invalid/missing
        """
        pos_order_sudo = self._check_order_access(pos_order_id, access_token)
        self._ensure_session_open(pos_order_sudo)
        exit_route = request.httprequest.args.get('exit_route')
        user_sudo = request.env.user
        if not pos_order_sudo.partner_id:
            user_sudo = request.env.ref('base.public_user')
        logged_in = not user_sudo._is_public()
        partner_sudo = pos_order_sudo.partner_id or self._get_partner_sudo(user_sudo)
        if not partner_sudo:
            return self._redirect_login()

        self._validate_transaction_kwargs(kwargs)
        if kwargs.get('is_validation'):
            raise UserError(
                _("A validation payment cannot be used for a Point of Sale online payment."))

        if 'partner_id' in kwargs and kwargs['partner_id'] != partner_sudo.id:
            raise UserError(
                _("The provided partner_id is different than expected."))
        # Avoid tokenization for the public user.
        kwargs.update({
            'partner_id': partner_sudo.id,
            'partner_phone': partner_sudo.phone,
            'custom_create_values': {
                'pos_order_id': pos_order_sudo.id,
            },
        })
        if not logged_in:
            if kwargs.get('tokenization_requested') or kwargs.get('flow') == 'token':
                raise UserError(
                    _("Tokenization is not available for logged out customers."))
            kwargs['custom_create_values']['tokenize'] = False

        currency_id = pos_order_sudo.currency_id
        if not currency_id.active:
            raise ValidationError(_("The currency is invalid."))
        # Ignore the currency provided by the customer
        kwargs['currency_id'] = currency_id.id

        amount_to_pay = self._get_amount_to_pay(pos_order_sudo)
        if not self._is_valid_amount(amount_to_pay, currency_id):
            raise ValidationError(_("There is nothing to pay for this order."))
        if tools.float_compare(kwargs['amount'], amount_to_pay, precision_rounding=currency_id.rounding) != 0:
            raise ValidationError(
                _("The amount to pay has changed. Please refresh the page."))

        payment_option_id = kwargs.get('payment_method_id') or kwargs.get('token_id')
        if not payment_option_id:
            raise UserError(_("A payment option must be specified."))
        flow = kwargs.get('flow')
        if not (flow and flow in ['redirect', 'direct', 'token']):
            raise UserError(_("The payment should either be direct, with redirection, or made by a token."))
        providers_sudo = self._get_allowed_providers_sudo(pos_order_sudo, partner_sudo.id, amount_to_pay)
        if flow == 'token':
            tokens_sudo = request.env['payment.token']._get_available_tokens(
                providers_sudo.ids, partner_sudo.id)
            if payment_option_id not in tokens_sudo.ids:
                raise UserError(_("The payment token is invalid."))
        else:
            if kwargs.get('provider_id') not in providers_sudo.ids:
                raise UserError(_("The payment provider is invalid."))

        kwargs['reference_prefix'] = None  # Computed with pos_order_id
        kwargs.pop('pos_order_id', None) # _create_transaction kwargs keys must be different than custom_create_values keys

        tx_sudo = self._create_transaction(**kwargs)
        tx_sudo.landing_route = PaymentPortal._get_landing_route(pos_order_sudo.id, access_token, exit_route=exit_route, tx_id=tx_sudo.id)

        return tx_sudo._get_processing_values()

    @http.route('/pos/pay/confirmation/<int:pos_order_id>', type='http', methods=['GET'], auth='public', website=True, sitemap=False)
    def pos_order_pay_confirmation(self, pos_order_id, tx_id=None, access_token=None, exit_route=None, **kwargs):
        """ Behaves like payment.PaymentPortal.payment_confirm but for POS online payment.

        :param int pos_order_id: The POS order to confirm, as a `pos.order` id
        :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 str exit_route: The URL to open to leave the POS online payment flow
        :param dict kwargs: Data from payment module

        :return: The rendered confirmation page
        :rtype: str
        :raise: AccessError if the provided order or access token is invalid
        """
        tx_id = self._cast_as_int(tx_id)
        rendering_context = {
            'state': 'error',
            'exit_route': exit_route,
            'pay_route': self._get_pay_route(pos_order_id, access_token, exit_route)
        }
        if not tx_id or not pos_order_id:
            return self._render_pay_confirmation(rendering_context)

        pos_order_sudo = self._check_order_access(pos_order_id, access_token)

        tx_sudo = request.env['payment.transaction'].sudo().search([('id', '=', tx_id)])
        if tx_sudo.pos_order_id.id != pos_order_sudo.id:
            return self._render_pay_confirmation(rendering_context)

        rendering_context.update(
            pos_order_id=pos_order_sudo.id,
            order_reference=pos_order_sudo.pos_reference,
            tx_reference=tx_sudo.reference,
            amount=tx_sudo.amount,
            currency=tx_sudo.currency_id,
            provider_name=tx_sudo.provider_id.name,
            tx=tx_sudo, # for the payment.state_header template
        )

        if tx_sudo.state not in ('authorized', 'done'):
            rendering_context['state'] = 'tx_error'
            return self._render_pay_confirmation(rendering_context)

        tx_sudo._process_pos_online_payment()

        rendering_context['state'] = 'success'
        return self._render_pay_confirmation(rendering_context)

    def _render_pay_confirmation(self, rendering_context):
        return request.render('pos_online_payment.pay_confirmation', rendering_context)