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)
|