Currently if an error occurs on the server side it is not reported to the POS interface and therefore to the user. With this commit all responses to requests to the Stripe server are managed in the POS so that the cashier is correctly informed of any errors that may occur closes odoo/odoo#104870 X-original-commit: a9c794ed31cfe1527f0e480729212859338bca60 Signed-off-by: Trinh Jacky (trj) <trj@odoo.com>
125 lines
5.2 KiB
Python
125 lines
5.2 KiB
Python
# coding: utf-8
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import logging
|
|
import requests
|
|
import werkzeug
|
|
|
|
from odoo import fields, models, api, _
|
|
from odoo.exceptions import ValidationError, UserError, AccessError
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
TIMEOUT = 10
|
|
|
|
class PosPaymentMethod(models.Model):
|
|
_inherit = 'pos.payment.method'
|
|
|
|
def _get_payment_terminal_selection(self):
|
|
return super()._get_payment_terminal_selection() + [('stripe', 'Stripe')]
|
|
|
|
# Stripe
|
|
stripe_serial_number = fields.Char(help='[Serial number of the stripe terminal], for example: WSC513105011295', copy=False)
|
|
|
|
@api.constrains('stripe_serial_number')
|
|
def _check_stripe_serial_number(self):
|
|
for payment_method in self:
|
|
if not payment_method.stripe_serial_number:
|
|
continue
|
|
existing_payment_method = self.search([('id', '!=', payment_method.id),
|
|
('stripe_serial_number', '=', payment_method.stripe_serial_number)],
|
|
limit=1)
|
|
if existing_payment_method:
|
|
raise ValidationError(_('Terminal %s is already used on payment method %s.',\
|
|
payment_method.stripe_serial_number, existing_payment_method.display_name))
|
|
|
|
|
|
@api.model
|
|
def _get_stripe_secret_key(self):
|
|
stripe_secret_key = self.env['payment.provider'].search([('code', '=', 'stripe')], limit=1).stripe_secret_key
|
|
|
|
if not stripe_secret_key:
|
|
raise ValidationError(_('Complete the Stripe onboarding.'))
|
|
|
|
return stripe_secret_key
|
|
|
|
@api.model
|
|
def stripe_connection_token(self):
|
|
if not self.env.user.has_group('point_of_sale.group_pos_user'):
|
|
raise AccessError(_("Do not have access to fetch token from Stripe"))
|
|
|
|
endpoint = 'https://api.stripe.com/v1/terminal/connection_tokens'
|
|
|
|
try:
|
|
resp = requests.post(endpoint, auth=(self.sudo()._get_stripe_secret_key(), ''), timeout=TIMEOUT)
|
|
except requests.exceptions.RequestException:
|
|
_logger.exception("Failed to call stripe_connection_token endpoint")
|
|
raise UserError(_("There are some issues between us and Stripe, try again later."))
|
|
|
|
return resp.json()
|
|
|
|
def _stripe_calculate_amount(self, amount):
|
|
currency = self.journal_id.currency_id or self.company_id.currency_id
|
|
return int(amount/currency.rounding)
|
|
|
|
def stripe_payment_intent(self, amount):
|
|
if not self.env.user.has_group('point_of_sale.group_pos_user'):
|
|
raise AccessError(_("Do not have access to fetch token from Stripe"))
|
|
|
|
# For Terminal payments, the 'payment_method_types' parameter must include
|
|
# 'card_present' and the 'capture_method' must be set to 'manual'
|
|
endpoint = 'https://api.stripe.com/v1/payment_intents'
|
|
currency = self.journal_id.currency_id or self.company_id.currency_id
|
|
|
|
try:
|
|
data = werkzeug.urls.url_encode({
|
|
"currency": currency.name,
|
|
"amount": self._stripe_calculate_amount(amount),
|
|
"payment_method_types[]": "card_present",
|
|
"capture_method": "manual",
|
|
})
|
|
resp = requests.post(endpoint, data=data, auth=(self.sudo()._get_stripe_secret_key(), ''), timeout=TIMEOUT)
|
|
except requests.exceptions.RequestException:
|
|
_logger.exception("Failed to call stripe_payment_intent endpoint")
|
|
raise UserError(_("There are some issues between us and Stripe, try again later."))
|
|
|
|
return resp.json()
|
|
|
|
@api.model
|
|
def stripe_capture_payment(self, paymentIntentId, amount=None):
|
|
"""Captures the payment identified by paymentIntentId.
|
|
|
|
:param paymentIntentId: the id of the payment to capture
|
|
:param amount: without this parameter the entire authorized
|
|
amount is captured. Specifying a larger amount allows
|
|
overcapturing to support tips.
|
|
"""
|
|
if not self.env.user.has_group('point_of_sale.group_pos_user'):
|
|
raise AccessError(_("Do not have access to fetch token from Stripe"))
|
|
|
|
endpoint = ('https://api.stripe.com/v1/payment_intents/%s/capture') % \
|
|
(werkzeug.urls.url_quote(paymentIntentId))
|
|
|
|
data = None
|
|
if amount is not None:
|
|
data = {
|
|
"amount_to_capture": self._stripe_calculate_amount(amount),
|
|
}
|
|
|
|
try:
|
|
resp = requests.post(endpoint, data=data, auth=(self.sudo()._get_stripe_secret_key(), ''), timeout=TIMEOUT)
|
|
except requests.exceptions.RequestException:
|
|
_logger.exception("Failed to call stripe_capture_payment endpoint")
|
|
raise UserError(_("There are some issues between us and Stripe, try again later."))
|
|
|
|
return resp.json()
|
|
|
|
def action_stripe_key(self):
|
|
res_id = self.env['payment.provider'].search([('code', '=', 'stripe')], limit=1).id
|
|
# Redirect
|
|
return {
|
|
'name': _('Stripe'),
|
|
'res_model': 'payment.provider',
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'form',
|
|
'res_id': res_id,
|
|
}
|