# -*- 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 canceled.")) 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): partner_sudo = user_sudo.partner_id if not partner_sudo and user_sudo._is_public(): partner_sudo = self.env.ref('base.public_user') return partner_sudo 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/', 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 logged_in = not user_sudo._is_public() partner_sudo = 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/', 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 logged_in = not user_sudo._is_public() partner_sudo = 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/', 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.transaction_status 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)