295 lines
14 KiB
Python
295 lines
14 KiB
Python
|
# -*- 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/<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
|
||
|
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/<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
|
||
|
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/<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.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)
|