diff --git a/addons/pos_adyen/controllers/main.py b/addons/pos_adyen/controllers/main.py index 2fb43e8c245..4fe6988bfba 100644 --- a/addons/pos_adyen/controllers/main.py +++ b/addons/pos_adyen/controllers/main.py @@ -2,14 +2,17 @@ import logging import pprint import json +from urllib.parse import parse_qs from odoo import http from odoo.http import request +from odoo.tools import consteq _logger = logging.getLogger(__name__) class PosAdyenController(http.Controller): - @http.route('/pos_adyen/notification', type='json', methods=['POST'], auth='none', csrf=False) + + @http.route('/pos_adyen/notification', type='json', methods=['POST'], auth='public', csrf=False, save_session=False) def notification(self): data = json.loads(request.httprequest.data) @@ -18,18 +21,60 @@ class PosAdyenController(http.Controller): return _logger.info('notification received from adyen:\n%s', pprint.pformat(data)) - terminal_identifier = data['SaleToPOIResponse']['MessageHeader']['POIID'] - payment_method_sudo = request.env['pos.payment.method'].sudo().search([('adyen_terminal_identifier', '=', terminal_identifier)], limit=1) - pos_session_id = int(data["SaleToPOIResponse"]["PaymentResponse"]["SaleData"]["SaleTransactionID"]["TransactionID"].split("-")[1]) - pos_session_sudo = request.env["pos.session"].sudo().browse(pos_session_id) - if payment_method_sudo: - # These are only used to see if the terminal is reachable, - # store the most recent ID we received. - if data['SaleToPOIResponse'].get('DiagnosisResponse'): - payment_method_sudo.adyen_latest_diagnosis = data['SaleToPOIResponse']['MessageHeader']['ServiceID'] - else: - payment_method_sudo.adyen_latest_response = json.dumps(data) - request.env["bus.bus"].sudo()._sendone(pos_session_sudo._get_bus_channel_name(), "ADYEN_LATEST_RESPONSE", pos_session_sudo.config_id.id) - else: - _logger.error('received a message for a terminal not registered in Odoo: %s', terminal_identifier) + msg_header = data['SaleToPOIResponse'].get('MessageHeader') + if not msg_header \ + or msg_header.get('ProtocolVersion') != '3.0' \ + or msg_header.get('MessageClass') != 'Service' \ + or msg_header.get('MessageType') != 'Response' \ + or msg_header.get('MessageCategory') != 'Payment' \ + or not msg_header.get('POIID'): + _logger.warning('Received an unexpected Adyen notification') + return + + terminal_identifier = msg_header['POIID'] + adyen_pm_sudo = request.env['pos.payment.method'].sudo().search([('adyen_terminal_identifier', '=', terminal_identifier)], limit=1) + if not adyen_pm_sudo: + _logger.warning('Received an Adyen event notification for a terminal not registered in Odoo: %s', terminal_identifier) + return + + try: + adyen_additional_response = data['SaleToPOIResponse']['PaymentResponse']['Response']['AdditionalResponse'] + pos_hmac = PosAdyenController._get_additional_data_from_unparsed(adyen_additional_response, 'metadata.pos_hmac') + + if not pos_hmac or not consteq(pos_hmac, adyen_pm_sudo._get_hmac(msg_header['SaleID'], msg_header['ServiceID'], msg_header['POIID'], data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID'])): + _logger.warning('Received an invalid Adyen event notification (invalid hmac): \n%s', pprint.pformat(data)) + return + + # The HMAC is removed to prevent anyone from using it in place of Adyen. + pos_hmac_metadata_raw = 'metadata.pos_hmac='+pos_hmac + safe_additional_response = adyen_additional_response.replace('&'+pos_hmac_metadata_raw, '').replace(pos_hmac_metadata_raw, '') + data['SaleToPOIResponse']['PaymentResponse']['Response']['AdditionalResponse'] = safe_additional_response + except (KeyError, AttributeError): + _logger.warning('Received an invalid Adyen event notification: \n%s', pprint.pformat(data)) + return + + return self._process_payment_response(data, adyen_pm_sudo) + + @staticmethod + def _get_additional_data_from_unparsed(adyen_additional_response, data_key): + parsed_adyen_additional_response = parse_qs(adyen_additional_response) + return PosAdyenController._get_additional_data_from_parsed(parsed_adyen_additional_response, data_key) + + @staticmethod + def _get_additional_data_from_parsed(parsed_adyen_additional_response, data_key): + data_value = parsed_adyen_additional_response.get(data_key) + return data_value[0] if data_value and len(data_value) == 1 else None + + def _process_payment_response(self, data, adyen_pm_sudo): + transaction_id = data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID'] + if not transaction_id: + return + transaction_id_parts = transaction_id.split("--") + if len(transaction_id_parts) != 2: + return + pos_session_id = int(transaction_id_parts[1]) + pos_session_sudo = request.env["pos.session"].sudo().browse(pos_session_id) + adyen_pm_sudo.adyen_latest_response = json.dumps(data) + request.env['bus.bus'].sudo()._sendone(pos_session_sudo._get_bus_channel_name(), 'ADYEN_LATEST_RESPONSE', pos_session_sudo.config_id.id) + return request.make_json_response('[accepted]') # https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/#guarantee diff --git a/addons/pos_adyen/models/pos_payment_method.py b/addons/pos_adyen/models/pos_payment_method.py index 56144756a06..01846c113be 100644 --- a/addons/pos_adyen/models/pos_payment_method.py +++ b/addons/pos_adyen/models/pos_payment_method.py @@ -6,13 +6,17 @@ import pprint import random import requests import string +from urllib.parse import parse_qs from werkzeug.exceptions import Forbidden from odoo import fields, models, api, _ -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, UserError, AccessDenied +from odoo.tools import hmac _logger = logging.getLogger(__name__) +UNPREDICTABLE_ADYEN_DATA = object() # sentinel + class PosPaymentMethod(models.Model): _inherit = 'pos.payment.method' @@ -20,9 +24,9 @@ class PosPaymentMethod(models.Model): return super(PosPaymentMethod, self)._get_payment_terminal_selection() + [('adyen', 'Adyen')] # Adyen - adyen_api_key = fields.Char(string="Adyen API key", help='Used when connecting to Adyen: https://docs.adyen.com/user-management/how-to-get-the-api-key/#description', copy=False) + adyen_api_key = fields.Char(string="Adyen API key", help='Used when connecting to Adyen: https://docs.adyen.com/user-management/how-to-get-the-api-key/#description', copy=False, groups='base.group_erp_manager') adyen_terminal_identifier = fields.Char(help='[Terminal model]-[Serial number], for example: P400Plus-123456789', copy=False) - adyen_test_mode = fields.Boolean(help='Run transactions in the test environment.') + adyen_test_mode = fields.Boolean(help='Run transactions in the test environment.', groups='base.group_erp_manager') adyen_latest_response = fields.Char(copy=False, groups='base.group_erp_manager') # used to buffer the latest asynchronous notification from Adyen. adyen_latest_diagnosis = fields.Char(copy=False, groups='base.group_erp_manager') # used to determine if the terminal is still connected. @@ -52,54 +56,179 @@ class PosPaymentMethod(models.Model): } def _is_write_forbidden(self, fields): - whitelisted_fields = {'adyen_latest_response', 'adyen_latest_diagnosis'} - return super(PosPaymentMethod, self)._is_write_forbidden(fields - whitelisted_fields) - - def _adyen_diagnosis_request_data(self, pos_config_name): - service_id = ''.join(random.choices(string.ascii_letters + string.digits, k=10)) - return { - "SaleToPOIRequest": { - "MessageHeader": { - "ProtocolVersion": "3.0", - "MessageClass": "Service", - "MessageCategory": "Diagnosis", - "MessageType": "Request", - "ServiceID": service_id, - "SaleID": pos_config_name, - "POIID": self.adyen_terminal_identifier, - }, - "DiagnosisRequest": { - "HostDiagnosisFlag": False - } - } - } + return super(PosPaymentMethod, self)._is_write_forbidden(fields - {'adyen_latest_response'}) def get_latest_adyen_status(self): self.ensure_one() + if not self.env.su and not self.user_has_groups('point_of_sale.group_pos_user'): + raise AccessDenied() + latest_response = self.sudo().adyen_latest_response latest_response = json.loads(latest_response) if latest_response else False return latest_response def proxy_adyen_request(self, data, operation=False): ''' Necessary because Adyen's endpoints don't have CORS enabled ''' - if data.get('SaleToPOIRequest') and data['SaleToPOIRequest']['MessageHeader']['MessageCategory'] == 'Payment': # Clear only if it is a payment request + self.ensure_one() + if not self.env.su and not self.user_has_groups('point_of_sale.group_pos_user'): + raise AccessDenied() + if not data: + raise UserError(_('Invalid Adyen request')) + + if 'SaleToPOIRequest' in data and data['SaleToPOIRequest']['MessageHeader']['MessageCategory'] == 'Payment': # Clear only if it is a payment request self.sudo().adyen_latest_response = '' # avoid handling old responses multiple times if not operation: operation = 'terminal_request' + # These checks are not optimal. This RPC method should be changed. + + is_capture_data = operation == 'capture' and hasattr(self, 'adyen_merchant_account') and self._is_valid_adyen_request_data(data, { + 'originalReference': UNPREDICTABLE_ADYEN_DATA, + 'modificationAmount': { + 'value': UNPREDICTABLE_ADYEN_DATA, + 'currency': UNPREDICTABLE_ADYEN_DATA, + }, + 'merchantAccount': self.adyen_merchant_account, + }) + + is_adjust_data = operation == 'adjust' and hasattr(self, 'adyen_merchant_account') and self._is_valid_adyen_request_data(data, { + 'originalReference': UNPREDICTABLE_ADYEN_DATA, + 'modificationAmount': { + 'value': UNPREDICTABLE_ADYEN_DATA, + 'currency': UNPREDICTABLE_ADYEN_DATA, + }, + 'merchantAccount': self.adyen_merchant_account, + 'additionalData': { + 'industryUsage': 'DelayedCharge', + }, + }) + + is_cancel_data = operation == 'terminal_request' and self._is_valid_adyen_request_data(data, { + 'SaleToPOIRequest': { + 'MessageHeader': self._get_expected_message_header('Abort'), + 'AbortRequest': { + 'AbortReason': 'MerchantAbort', + 'MessageReference': { + 'MessageCategory': 'Payment', + 'SaleID': UNPREDICTABLE_ADYEN_DATA, + 'ServiceID': UNPREDICTABLE_ADYEN_DATA, + }, + }, + }, + }) + + is_payment_request_with_acquirer_data = operation == 'terminal_request' and self._is_valid_adyen_request_data(data, self._get_expected_payment_request(True)) + + if is_payment_request_with_acquirer_data: + parsed_sale_to_acquirer_data = parse_qs(data['SaleToPOIRequest']['PaymentRequest']['SaleData']['SaleToAcquirerData']) + valid_acquirer_data = self._get_valid_acquirer_data() + is_payment_request_with_acquirer_data = len(parsed_sale_to_acquirer_data.keys()) <= len(valid_acquirer_data.keys()) + if is_payment_request_with_acquirer_data: + for key, values in parsed_sale_to_acquirer_data.items(): + if len(values) != 1: + is_payment_request_with_acquirer_data = False + break + value = values[0] + valid_value = valid_acquirer_data.get(key) + if valid_value == UNPREDICTABLE_ADYEN_DATA: + continue + if value != valid_value: + is_payment_request_with_acquirer_data = False + break + + is_payment_request_without_acquirer_data = operation == 'terminal_request' and self._is_valid_adyen_request_data(data, self._get_expected_payment_request(False)) + + if not is_payment_request_without_acquirer_data and not is_payment_request_with_acquirer_data and not is_adjust_data and not is_cancel_data and not is_capture_data: + raise UserError(_('Invalid Adyen request')) + + if is_payment_request_with_acquirer_data or is_payment_request_without_acquirer_data: + acquirer_data = data['SaleToPOIRequest']['PaymentRequest']['SaleData'].get('SaleToAcquirerData') + msg_header = data['SaleToPOIRequest']['MessageHeader'] + metadata = 'metadata.pos_hmac=' + self._get_hmac(msg_header['SaleID'], msg_header['ServiceID'], msg_header['POIID'], data['SaleToPOIRequest']['PaymentRequest']['SaleData']['SaleTransactionID']['TransactionID']) + + data['SaleToPOIRequest']['PaymentRequest']['SaleData']['SaleToAcquirerData'] = acquirer_data + '&' + metadata if acquirer_data else metadata + return self._proxy_adyen_request_direct(data, operation) + @api.model + def _is_valid_adyen_request_data(self, provided_data, expected_data): + if not isinstance(provided_data, dict) or set(provided_data.keys()) != set(expected_data.keys()): + return False + + for provided_key, provided_value in provided_data.items(): + expected_value = expected_data[provided_key] + if expected_value == UNPREDICTABLE_ADYEN_DATA: + continue + if isinstance(expected_value, dict): + if not self._is_valid_adyen_request_data(provided_value, expected_value): + return False + else: + if provided_value != expected_value: + return False + return True + + def _get_expected_message_header(self, expected_message_category): + return { + 'ProtocolVersion': '3.0', + 'MessageClass': 'Service', + 'MessageType': 'Request', + 'MessageCategory': expected_message_category, + 'SaleID': UNPREDICTABLE_ADYEN_DATA, + 'ServiceID': UNPREDICTABLE_ADYEN_DATA, + 'POIID': self.adyen_terminal_identifier, + } + + def _get_expected_payment_request(self, with_acquirer_data): + res = { + 'SaleToPOIRequest': { + 'MessageHeader': self._get_expected_message_header('Payment'), + 'PaymentRequest': { + 'SaleData': { + 'SaleTransactionID': { + 'TransactionID': UNPREDICTABLE_ADYEN_DATA, + 'TimeStamp': UNPREDICTABLE_ADYEN_DATA, + }, + }, + 'PaymentTransaction': { + 'AmountsReq': { + 'Currency': UNPREDICTABLE_ADYEN_DATA, + 'RequestedAmount': UNPREDICTABLE_ADYEN_DATA, + }, + }, + }, + }, + } + + if with_acquirer_data: + res['SaleToPOIRequest']['PaymentRequest']['SaleData']['SaleToAcquirerData'] = UNPREDICTABLE_ADYEN_DATA + return res + + @api.model + def _get_valid_acquirer_data(self): + return { + 'tenderOption': 'AskGratuity', + 'authorisationType': 'PreAuth' + } + + @api.model + def _get_hmac(self, sale_id, service_id, poi_id, sale_transaction_id): + return hmac( + env=self.env(su=True), + scope='pos_adyen_payment', + message=(sale_id, service_id, poi_id, sale_transaction_id) + ) + def _proxy_adyen_request_direct(self, data, operation): self.ensure_one() TIMEOUT = 10 - _logger.info('request to adyen\n%s', pprint.pformat(data)) + _logger.info('Request to Adyen by user #%d:\n%s', self.env.uid, pprint.pformat(data)) - environment = 'test' if self.adyen_test_mode else 'live' + environment = 'test' if self.sudo().adyen_test_mode else 'live' endpoint = self._get_adyen_endpoints()[operation] % environment headers = { - 'x-api-key': self.adyen_api_key, + 'x-api-key': self.sudo().adyen_api_key, } req = requests.post(endpoint, json=data, headers=headers, timeout=TIMEOUT) diff --git a/addons/pos_adyen/static/src/app/payment_adyen.js b/addons/pos_adyen/static/src/app/payment_adyen.js index 5b4755265f5..74a4aa16bfe 100644 --- a/addons/pos_adyen/static/src/app/payment_adyen.js +++ b/addons/pos_adyen/static/src/app/payment_adyen.js @@ -264,7 +264,7 @@ export class PaymentAdyen extends PaymentInterface { const payment_result = payment_response.PaymentResult; const cashier_receipt = payment_response.PaymentReceipt.find((receipt) => { - receipt.DocumentQualifier == "CashierReceipt"; + return receipt.DocumentQualifier == "CashierReceipt"; }); if (cashier_receipt) { @@ -274,7 +274,7 @@ export class PaymentAdyen extends PaymentInterface { } const customer_receipt = payment_response.PaymentReceipt.find((receipt) => { - receipt.DocumentQualifier == "CustomerReceipt"; + return receipt.DocumentQualifier == "CustomerReceipt"; }); if (customer_receipt) { diff --git a/addons/pos_adyen/views/pos_payment_method_views.xml b/addons/pos_adyen/views/pos_payment_method_views.xml index f6ac20bfdef..d27778abbc0 100644 --- a/addons/pos_adyen/views/pos_payment_method_views.xml +++ b/addons/pos_adyen/views/pos_payment_method_views.xml @@ -7,7 +7,7 @@ - + diff --git a/addons/pos_self_order/controllers/orders.py b/addons/pos_self_order/controllers/orders.py index ad4aa635a38..ee364c7f4d1 100644 --- a/addons/pos_self_order/controllers/orders.py +++ b/addons/pos_self_order/controllers/orders.py @@ -193,7 +193,7 @@ class PosSelfOrderController(http.Controller): if not order_sudo or not payment_method_sudo or payment_method_sudo not in order_sudo.config_id.payment_method_ids: raise NotFound("Order or payment method not found") - status = payment_method_sudo.payment_request_from_kiosk(order_sudo) + status = payment_method_sudo._payment_request_from_kiosk(order_sudo) if not status: raise BadRequest("Something went wrong") diff --git a/addons/pos_self_order/models/pos_payment_method.py b/addons/pos_self_order/models/pos_payment_method.py index 2a619307e39..9c7915ac180 100644 --- a/addons/pos_self_order/models/pos_payment_method.py +++ b/addons/pos_self_order/models/pos_payment_method.py @@ -5,5 +5,5 @@ class PosPaymentMethod(models.Model): _inherit = "pos.payment.method" # will be overridden. - def payment_request_from_kiosk(self, order): + def _payment_request_from_kiosk(self, order): pass diff --git a/addons/pos_self_order_adyen/controllers/main.py b/addons/pos_self_order_adyen/controllers/main.py index fe445effb95..d0d748996ad 100644 --- a/addons/pos_self_order_adyen/controllers/main.py +++ b/addons/pos_self_order_adyen/controllers/main.py @@ -1,43 +1,53 @@ -import json +import logging from odoo.addons.pos_adyen.controllers.main import PosAdyenController -from odoo import http, fields +from odoo import fields from odoo.http import request +_logger = logging.getLogger(__name__) + class PosSelfAdyenController(PosAdyenController): - @http.route() - def notification(self): - super().notification() - data = json.loads(request.httprequest.data) - if data.get('SaleToPOIResponse'): - order_reference = data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID'] - payment_result = data['SaleToPOIResponse']['PaymentResponse']['Response']['Result'] - order_sudo = request.env['pos.order'].sudo().search([('pos_reference', '=', order_reference)], limit=1) - order = order_sudo.sudo(False).with_user(order_sudo.session_id.config_id.self_ordering_default_user_id).with_company(order_sudo.session_id.config_id.company_id) - terminal_identifier = data['SaleToPOIResponse']['MessageHeader']['POIID'] - payment_method = request.env['pos.payment.method'].sudo().search([('adyen_terminal_identifier', '=', terminal_identifier)], limit=1) + def _process_payment_response(self, data, adyen_pm_sudo): + self_order_id = None + try: + self_order_id = PosAdyenController._get_additional_data_from_unparsed(data['SaleToPOIResponse']['PaymentResponse']['Response']['AdditionalResponse'], 'metadata.self_order_id') + except KeyError: + self_order_id = None - if payment_result == 'Success' and order.config_id.self_ordering_mode == 'kiosk': - payment_amount = data['SaleToPOIResponse']['PaymentResponse']['PaymentResult']['AmountsResp']['AuthorizedAmount'] - card_type = data['SaleToPOIResponse']['PaymentResponse']['PaymentResult']['PaymentInstrumentData']['CardData']['PaymentBrand'] - transaction_id = data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID'] - order.add_payment({ - 'amount': payment_amount, - 'payment_date': fields.Datetime.now(), - 'payment_method_id': payment_method.id, - 'card_type': card_type, - 'cardholder_name': '', - 'transaction_id': transaction_id, - 'payment_status': payment_result, - 'ticket': '', - 'pos_order_id': order.id - }) - order.action_pos_order_paid() - order._send_order() + if not self_order_id: + return super()._process_payment_response(data, adyen_pm_sudo) - if order.config_id.self_ordering_mode == 'kiosk': - order.env['bus.bus']._sendone(f'pos_config-{order.config_id.access_token}', 'PAYMENT_STATUS', { - 'payment_result': payment_result, - 'order': order._export_for_self_order(), - }) + order_sudo = request.env['pos.order'].sudo().search([('id', '=', self_order_id)], limit=1) + if not order_sudo: + _logger.warning('Received an Adyen event notification for the self order #%d that does not exist (anymore)', self_order_id) + return request.make_json_response('[accepted]') # https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/#guarantee + + order = order_sudo.sudo(False).with_user(order_sudo.session_id.config_id.self_ordering_default_user_id).with_company(order_sudo.session_id.config_id.company_id) + + payment_result = data['SaleToPOIResponse']['PaymentResponse']['Response']['Result'] + + if payment_result == 'Success' and order.config_id.self_ordering_mode == 'kiosk': + payment_amount = data['SaleToPOIResponse']['PaymentResponse']['PaymentResult']['AmountsResp']['AuthorizedAmount'] + card_type = data['SaleToPOIResponse']['PaymentResponse']['PaymentResult']['PaymentInstrumentData']['CardData']['PaymentBrand'] + transaction_id = data['SaleToPOIResponse']['PaymentResponse']['SaleData']['SaleTransactionID']['TransactionID'] + order.add_payment({ + 'amount': payment_amount, + 'payment_date': fields.Datetime.now(), + 'payment_method_id': adyen_pm_sudo.id, + 'card_type': card_type, + 'cardholder_name': '', + 'transaction_id': transaction_id, + 'payment_status': payment_result, + 'ticket': '', + 'pos_order_id': order.id + }) + order.action_pos_order_paid() + order._send_order() + + if order.config_id.self_ordering_mode == 'kiosk': + order.env['bus.bus']._sendone(f'pos_config-{order.config_id.access_token}', 'PAYMENT_STATUS', { + 'payment_result': payment_result, + 'order': order._export_for_self_order(), + }) + return request.make_json_response('[accepted]') # https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/#guarantee diff --git a/addons/pos_self_order_adyen/models/pos_payment_method.py b/addons/pos_self_order_adyen/models/pos_payment_method.py index 2058ae8d9aa..d759ad830af 100644 --- a/addons/pos_self_order_adyen/models/pos_payment_method.py +++ b/addons/pos_self_order_adyen/models/pos_payment_method.py @@ -1,14 +1,21 @@ from datetime import datetime, timezone import random -from odoo import models +from odoo import models, api +from odoo.addons.pos_adyen.models.pos_payment_method import UNPREDICTABLE_ADYEN_DATA class PosPaymentMethod(models.Model): _inherit = "pos.payment.method" - def payment_request_from_kiosk(self, order): + @api.model + def _get_valid_acquirer_data(self): + res = super()._get_valid_acquirer_data() + res['metadata.self_order_id'] = UNPREDICTABLE_ADYEN_DATA + return res + + def _payment_request_from_kiosk(self, order): if self.use_payment_terminal != 'adyen': - return super().payment_request_from_kiosk(order) + return super()._payment_request_from_kiosk(order) else: pos_config = order.session_id.config_id random_number = random.randrange(10**9, 10**10 - 1) @@ -31,6 +38,7 @@ class PosPaymentMethod(models.Model): 'TransactionID': order.pos_reference, # your reference to identify a payment. 'TimeStamp': datetime.now(tz=timezone.utc).isoformat(timespec='seconds'), # date and time of the request in UTC format. }, + 'SaleToAcquirerData': 'metadata.self_order_id=' + str(order.id), }, 'PaymentTransaction': { 'AmountsReq': { @@ -44,7 +52,4 @@ class PosPaymentMethod(models.Model): req = self.proxy_adyen_request(data) - if not req and req.get('error'): - return False - else: - return True + return req and (isinstance(req, bool) or not req.get('error')) diff --git a/addons/pos_self_order_stripe/models/pos_payment_method.py b/addons/pos_self_order_stripe/models/pos_payment_method.py index 47364852e61..17af357b614 100644 --- a/addons/pos_self_order_stripe/models/pos_payment_method.py +++ b/addons/pos_self_order_stripe/models/pos_payment_method.py @@ -4,8 +4,8 @@ from odoo import models class PosPaymentMethod(models.Model): _inherit = "pos.payment.method" - def payment_request_from_kiosk(self, order): + def _payment_request_from_kiosk(self, order): if self.use_payment_terminal != 'stripe': - return super().payment_request_from_kiosk(order) + return super()._payment_request_from_kiosk(order) else: return self.stripe_payment_intent(order.amount_total)