[IMP] payment, *: verify and reroute payment flows

This commit changes the way transactions linked to a document (sales
order, invoice...) are created in a payment flow. Rather than receiving
and trusting the transaction values from the controller, they are now
read from the linked document, and the payment flow is rerouted to use
the document's module's controllers instead of that of `payment`.

This ensures that no unexpected value can be passed to the `create`
method of a transaction, and simplifies the implementation of the
payment flows of linked documents.

task-3136240

closes odoo/odoo#126425

Related: odoo/enterprise#43212
Related: odoo/upgrade#5124
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Anita (anko)
2023-09-23 21:08:01 +00:00
committed by Antoine Vandevenne (anv)
parent 07fd6277f9
commit 4d1a1f1c99
27 changed files with 212 additions and 252 deletions
-6
View File
@@ -19,7 +19,6 @@
'views/account_move_views.xml',
'views/account_journal_views.xml',
'views/account_payment_views.xml',
'views/payment_form_templates.xml',
'views/payment_provider_views.xml',
'views/payment_transaction_views.xml',
@@ -28,11 +27,6 @@
'wizards/payment_refund_wizard_views.xml',
'wizards/res_config_settings_views.xml',
],
'assets': {
'web.assets_frontend': [
'account_payment/static/src/js/payment_form.js',
],
},
'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook',
'license': 'LGPL-3',
+21 -29
View File
@@ -30,11 +30,13 @@ class PaymentPortal(payment_portal.PaymentPortal):
except AccessError:
raise ValidationError(_("The access token is invalid."))
kwargs['reference_prefix'] = None # Allow the reference to be computed based on the invoice
logged_in = not request.env.user._is_public()
partner = request.env.user.partner_id if logged_in else invoice_sudo.partner_id
kwargs['partner_id'] = partner.id
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
partner_sudo = request.env.user.partner_id if logged_in else invoice_sudo.partner_id
self._validate_transaction_kwargs(kwargs)
kwargs.update({
'currency_id': invoice_sudo.currency_id.id,
'partner_id': partner_sudo.id,
}) # Inject the create values taken from the invoice into the kwargs.
tx_sudo = self._create_transaction(
custom_create_values={'invoice_ids': [Command.set([invoice_id])]}, **kwargs,
)
@@ -47,9 +49,6 @@ class PaymentPortal(payment_portal.PaymentPortal):
def payment_pay(self, *args, amount=None, invoice_id=None, access_token=None, **kwargs):
""" Override of `payment` to replace the missing transaction values by that of the invoice.
This is necessary for the reconciliation as all transaction values, excepted the amount,
need to match exactly that of the invoice.
:param str amount: The (possibly partial) amount to pay used to check the access token.
:param str invoice_id: The invoice for which a payment id made, as an `account.move` id.
:param str access_token: The access token used to authenticate the partner.
@@ -73,7 +72,11 @@ class PaymentPortal(payment_portal.PaymentPortal):
raise ValidationError(_("The provided parameters are invalid."))
kwargs.update({
# To display on the payment form; will be later overwritten when creating the tx.
'reference': invoice_sudo.name,
# To fix the currency if incorrect and avoid mismatches when creating the tx.
'currency_id': invoice_sudo.currency_id.id,
# To fix the partner if incorrect and avoid mismatches when creating the tx.
'partner_id': invoice_sudo.partner_id.id,
'company_id': invoice_sudo.company_id.id,
'invoice_id': invoice_id,
@@ -81,38 +84,27 @@ class PaymentPortal(payment_portal.PaymentPortal):
return super().payment_pay(*args, amount=amount, access_token=access_token, **kwargs)
def _get_extra_payment_form_values(self, invoice_id=None, **kwargs):
""" Override of `payment` to add the invoice id to the payment form values.
""" Override of `payment` to reroute the payment flow to the portal view of the invoice.
:param int invoice_id: The invoice for which a payment id made, as an `account.move` id.
:param str invoice_id: The invoice for which a payment id made, as an `account.move` id.
:param dict kwargs: Optional data. This parameter is not used here.
:return: The extended rendering context values.
:rtype: dict
"""
form_values = super()._get_extra_payment_form_values(invoice_id=invoice_id, **kwargs)
if invoice_id:
form_values['invoice_id'] = invoice_id
invoice_id = self._cast_as_int(invoice_id)
invoice_sudo = request.env['account.move'].sudo().browse(invoice_id)
# Interrupt the payment flow if the invoice has been canceled.
invoice_sudo = request.env['account.move'].sudo().browse(invoice_id)
if invoice_sudo.state == 'cancel':
form_values['amount'] = 0.0
# Reroute the next steps of the payment flow to the portal view of the invoice.
form_values.update({
'transaction_route': f'/invoice/transaction/{invoice_id}',
'landing_route': f'{invoice_sudo.access_url}'
f'?access_token={invoice_sudo._portal_ensure_token()}',
'access_token': invoice_sudo.access_token,
})
return form_values
def _create_transaction(self, *args, invoice_id=None, custom_create_values=None, **kwargs):
""" Override of `payment` to add the invoice id in the custom create values.
:param int invoice_id: The invoice for which a payment id made, as an `account.move` id.
:param dict custom_create_values: Additional create values overwriting the default ones.
:param dict kwargs: Optional data. This parameter is not used here.
:return: The result of the parent method.
:rtype: recordset of `payment.transaction`
"""
if invoice_id:
if custom_create_values is None:
custom_create_values = {}
custom_create_values['invoice_ids'] = [Command.set([int(invoice_id)])]
return super()._create_transaction(
*args, invoice_id=invoice_id, custom_create_values=custom_create_values, **kwargs
)
@@ -4,7 +4,6 @@ from odoo.http import request
from odoo.addons.account.controllers import portal
from odoo.addons.payment.controllers.portal import PaymentPortal
from odoo.addons.portal.controllers.portal import _build_url_w_params
class PortalAccount(portal.PortalAccount, PaymentPortal):
@@ -97,7 +97,6 @@ class AccountMove(models.Model):
def _get_default_payment_link_values(self):
self.ensure_one()
return {
'description': self.payment_reference,
'amount': self.amount_residual,
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.id,
@@ -1,25 +0,0 @@
/** @odoo-module **/
import paymentForm from '@payment/js/payment_form';
paymentForm.include({
// #=== PAYMENT FLOW ===#
/**
* Add `invoice_id` to the params for the RPC to the transaction route.
*
* @override method from @payment/js/payment_form
* @private
* @return {object} The extended transaction route params.
*/
_prepareTransactionRouteParams() {
const transactionRouteParams = this._super(...arguments);
return {
...transactionRouteParams,
'invoice_id': this.paymentContext['invoiceId']
? parseInt(this.paymentContext['invoiceId']) : null,
};
},
});
@@ -1,6 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.tests import tagged, JsonRpcException
from odoo.tools import mute_logger
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
@@ -17,33 +17,35 @@ class TestFlows(AccountPaymentCommon, PaymentHttpCommon):
route_values = self._prepare_pay_values()
route_values['invoice_id'] = self.invoice.id
tx_context = self._get_portal_pay_context(**route_values)
self.assertEqual(tx_context['invoice_id'], self.invoice.id)
# payment/transaction
route_values = {
k: tx_context[k]
for k in [
'amount',
'currency_id',
'reference_prefix',
'partner_id',
'access_token',
'landing_route',
'invoice_id',
]
}
route_values.update({
# /invoice/transaction/<id>
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': tx_context['amount'],
'flow': 'direct',
'tokenization_requested': False,
})
'landing_route': tx_context['landing_route'],
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(**route_values)
processing_values = self._get_processing_values(
tx_route=tx_context['transaction_route'], **tx_route_values
)
tx_sudo = self._get_tx(processing_values['reference'])
# Note: strangely, the check
# self.assertEqual(tx_sudo.invoice_ids, invoice)
# doesn't work, and cache invalidation doesn't work either.
self.invoice.invalidate_recordset(['transaction_ids'])
self.assertEqual(self.invoice.transaction_ids, tx_sudo)
@mute_logger('odoo.http')
def test_transaction_route_rejects_unexpected_kwarg(self):
url = self._build_url(f'/invoice/transaction/{self.invoice.id}/')
route_kwargs = {
'access_token': self.invoice._portal_ensure_token(),
'partner_id': self.partner.id, # This should be rejected.
}
with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
self.make_jsonrpc_request(url, route_kwargs)
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Include account-related values to the payment form to pass them to the client. -->
<template id="payment_form_inherit" inherit_id="payment.form">
<xpath expr="//form[@id='o_payment_form']" position="attributes">
<attribute name="t-att-data-invoice-id">invoice_id</attribute>
</xpath>
</template>
</odoo>
+37 -4
View File
@@ -260,7 +260,7 @@ class PaymentPortal(portal.CustomerPortal):
if not payment_utils.check_access_token(access_token, partner_id, amount, currency_id):
raise ValidationError(_("The access token is invalid."))
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
self._validate_transaction_kwargs(kwargs, additional_allowed_keys=('reference_prefix',))
tx_sudo = self._create_transaction(
amount=amount, currency_id=currency_id, partner_id=partner_id, **kwargs
)
@@ -268,8 +268,8 @@ class PaymentPortal(portal.CustomerPortal):
return tx_sudo._get_processing_values()
def _create_transaction(
self, provider_id, payment_method_id, token_id, reference_prefix, amount, currency_id,
partner_id, flow, tokenization_requested, landing_route, is_validation=False,
self, provider_id, payment_method_id, token_id, amount, currency_id, partner_id, flow,
tokenization_requested, landing_route, reference_prefix=None, is_validation=False,
custom_create_values=None, **kwargs
):
""" Create a draft transaction based on the payment context and return it.
@@ -278,7 +278,6 @@ class PaymentPortal(portal.CustomerPortal):
`payment.provider` id.
:param int|None payment_method_id: The payment method, if any, as a `payment.method` id.
:param int|None token_id: The token, if any, as a `payment.token` id.
:param str reference_prefix: The custom prefix to compute the full reference.
:param float|None amount: The amount to pay, or `None` if in a validation operation.
:param int|None currency_id: The currency of the amount, as a `res.currency` id, or `None`
if in a validation operation.
@@ -286,6 +285,7 @@ class PaymentPortal(portal.CustomerPortal):
:param str flow: The online payment flow of the transaction: 'redirect', 'direct' or 'token'.
:param bool tokenization_requested: Whether the user requested that a token is created.
:param str landing_route: The route the user is redirected to after the transaction.
:param str reference_prefix: The custom prefix to compute the full reference.
:param bool is_validation: Whether the operation is a validation.
:param dict custom_create_values: Additional create values overwriting the default ones.
:param dict kwargs: Locally unused data passed to `_is_tokenization_required` and
@@ -462,3 +462,36 @@ class PaymentPortal(portal.CustomerPortal):
:rtype: str
"""
return not partner.company_id or partner.company_id == document_company
@staticmethod
def _validate_transaction_kwargs(kwargs, additional_allowed_keys=()):
""" Verify that the keys of a transaction route's kwargs are all whitelisted.
The whitelist consists of all the keys that are expected to be passed to a transaction
route, plus optional contextually allowed keys.
This method must be called in all transaction routes to ensure that no undesired kwarg can
be passed as param and then injected in the create values of the transaction.
:param dict kwargs: The transaction route's kwargs to verify.
:param tuple additional_allowed_keys: The keys of kwargs that are contextually allowed.
:return: None
:raise ValidationError: If some kwargs keys are rejected.
"""
whitelist = {
'provider_id',
'payment_method_id',
'token_id',
'amount',
'flow',
'tokenization_requested',
'landing_route',
'is_validation',
'csrf_token',
}
whitelist.update(additional_allowed_keys)
rejected_keys = set(kwargs.keys()) - whitelist
if rejected_keys:
raise ValidationError(
_("The following kwargs are not whitelisted: %s", ', '.join(rejected_keys))
)
@@ -58,12 +58,9 @@ publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({
*/
_prepareTransactionRouteParams(providerId) {
return {
'payment_option_id': parseInt(providerId),
'reference_prefix': this.paymentContext['referencePrefix'] &&
this.paymentContext['referencePrefix'].toString(),
'currency_id': this.paymentContext['currencyId'] &&
parseInt(this.paymentContext['currencyId']),
'partner_id': parseInt(this.paymentContext['partnerId']),
'provider_id': parseInt(providerId),
'payment_method_id': 1, // TODO VCR
'token_id': null,
'flow': 'direct',
'tokenization_requested': false,
'landing_route': this.paymentContext['landingRoute'],
+11 -5
View File
@@ -407,16 +407,12 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
* @return {object} The transaction route params.
*/
_prepareTransactionRouteParams() {
return {
let transactionRouteParams = {
'provider_id': this.paymentContext.providerId,
'payment_method_id': this.paymentContext.paymentMethodId ?? null,
'token_id': this.paymentContext.tokenId ?? null,
'reference_prefix': this.paymentContext['referencePrefix']?.toString() ?? null,
'amount': this.paymentContext['amount'] !== undefined
? parseFloat(this.paymentContext['amount']) : null,
'currency_id': this.paymentContext['currencyId']
? parseInt(this.paymentContext['currencyId']) : null,
'partner_id': parseInt(this.paymentContext['partnerId']),
'flow': this.paymentContext['flow'],
'tokenization_requested': this.paymentContext['tokenizationRequested'],
'landing_route': this.paymentContext['landingRoute'],
@@ -424,6 +420,16 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
'access_token': this.paymentContext['accessToken'],
'csrf_token': odoo.csrf_token,
};
// Generic payment flows (i.e., that are not attached to a document) require extra params.
if (this.paymentContext['transactionRoute'] === '/payment/transaction') {
Object.assign(transactionRouteParams, {
'currency_id': this.paymentContext['currencyId']
? parseInt(this.paymentContext['currencyId']) : null,
'partner_id': parseInt(this.paymentContext['partnerId']),
'reference_prefix': this.paymentContext['referencePrefix']?.toString(),
});
}
return transactionRouteParams;
},
/**
+3 -4
View File
@@ -205,20 +205,19 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
'access_token': self._generate_test_access_token(
self.partner.id, self.amount, self.currency.id
),
'reference_prefix': 'test',
'tokenization_requested': True,
'landing_route': 'Test',
'reference_prefix': 'test',
'is_validation': False,
'flow': flow,
}
def _portal_transaction(self, **route_kwargs):
def _portal_transaction(self, tx_route='/payment/transaction', **route_kwargs):
"""/payment/transaction feedback
:return: The response to the json request
"""
uri = '/payment/transaction'
url = self._build_url(uri)
url = self._build_url(tx_route)
return self.make_jsonrpc_request(url, route_kwargs)
def _get_processing_values(self, **route_kwargs):
+14 -4
View File
@@ -37,10 +37,10 @@ class TestFlows(PaymentHttpCommon):
for k in [
'amount',
'currency_id',
'reference_prefix',
'partner_id',
'access_token',
'landing_route',
'reference_prefix',
'access_token',
]
}
route_values.update({
@@ -180,8 +180,8 @@ class TestFlows(PaymentHttpCommon):
'access_token': payment_context['access_token'],
'flow': flow,
'tokenization_requested': True,
'reference_prefix': payment_context['reference_prefix'],
'landing_route': payment_context['landing_route'],
'reference_prefix': payment_context['reference_prefix'],
'is_validation': True,
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
@@ -277,12 +277,13 @@ class TestFlows(PaymentHttpCommon):
def test_transaction_wrong_flow(self):
transaction_values = self._prepare_pay_values()
transaction_values.pop('reference')
transaction_values.update({
'flow': 'this flow does not exist',
'payment_option_id': self.provider.id,
'tokenization_requested': False,
'reference_prefix': 'whatever',
'landing_route': 'whatever',
'reference_prefix': 'whatever',
})
# Transaction step with a wrong flow --> UserError
with mute_logger("odoo.http"), self.assertRaises(
@@ -291,6 +292,15 @@ class TestFlows(PaymentHttpCommon):
):
self._portal_transaction(**transaction_values)
@mute_logger('odoo.http')
def test_transaction_route_rejects_unexpected_kwarg(self):
route_kwargs = {
**self._prepare_pay_values(),
'custom_create_values': 'whatever', # This should be rejected.
}
with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
self._portal_transaction(**route_kwargs)
def test_transaction_wrong_token(self):
route_values = self._prepare_pay_values()
route_values['access_token'] = "abcde"
@@ -64,10 +64,10 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
for k in [
'amount',
'currency_id',
'reference_prefix',
'partner_id',
'access_token',
'landing_route',
'reference_prefix',
'access_token',
]
}
validation_values.update({
@@ -30,7 +30,6 @@ class PaymentLinkWizard(models.TransientModel):
currency_id = fields.Many2one('res.currency')
partner_id = fields.Many2one('res.partner')
partner_email = fields.Char(related='partner_id.email')
description = fields.Char("Payment Ref")
link = fields.Char(string="Payment Link", compute='_compute_link')
company_id = fields.Many2one('res.company', compute='_compute_company_id')
warning_message = fields.Char(compute='_compute_warning_message')
@@ -58,13 +57,12 @@ class PaymentLinkWizard(models.TransientModel):
self.partner_id.id, self.amount, self.currency_id.id
)
@api.depends('description', 'amount', 'currency_id', 'partner_id', 'company_id')
@api.depends('amount', 'currency_id', 'partner_id', 'company_id')
def _compute_link(self):
for payment_link in self:
related_document = self.env[payment_link.res_model].browse(payment_link.res_id)
base_url = related_document.get_base_url() # Don't generate links for the wrong website
url_params = {
'reference': urls.url_quote(payment_link.description),
'amount': self.amount,
'access_token': self._get_access_token(),
**self._get_additional_link_values(),
@@ -19,7 +19,6 @@
<field name="partner_id" invisible="1"/>
<field name="partner_email" invisible="1"/>
<field name="amount_max" invisible="1"/>
<field name="description"/>
<field name="amount"/>
<field name="warning_message" invisible="1"/>
<field name="currency_id" invisible="1"/>
@@ -178,6 +178,7 @@ class PaymentPortal(payment_portal.PaymentPortal):
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."))
@@ -185,12 +186,13 @@ class PaymentPortal(payment_portal.PaymentPortal):
if 'partner_id' in kwargs and kwargs['partner_id'] != partner_sudo.id:
raise UserError(
_("The provided partner_id is different than expected."))
# Don't allow passing arbitrary create values and avoid tokenization for
# the public user.
kwargs['custom_create_values'] = {
'pos_order_id': pos_order_sudo.id
}
# Avoid tokenization for the public user.
kwargs.update({
'partner_id': partner_sudo.id,
'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(
@@ -37,9 +37,6 @@ class OnlinePaymentCommon(PaymentHttpCommon):
k: payment_context[k]
for k in [
'amount',
'currency_id',
'reference_prefix',
'partner_id',
'access_token',
'landing_route',
]
-2
View File
@@ -46,7 +46,6 @@ This module contains all the common features of Sales Management and eCommerce.
'views/account_views.xml',
'views/crm_team_views.xml',
'views/mail_activity_views.xml',
'views/payment_form_templates.xml',
'views/payment_views.xml',
'views/product_document_views.xml',
'views/product_packaging_views.xml',
@@ -79,7 +78,6 @@ This module contains all the common features of Sales Management and eCommerce.
'sale/static/src/js/sale_portal_sidebar.js',
'sale/static/src/js/sale_portal_prepayment.js',
'sale/static/src/js/sale_portal.js',
'sale/static/src/js/payment_form.js',
],
'web.assets_tests': [
'sale/static/tests/tours/**/*',
+20 -31
View File
@@ -360,12 +360,12 @@ class PaymentPortal(payment_portal.PaymentPortal):
logged_in = not request.env.user._is_public()
partner_sudo = request.env.user.partner_id if logged_in else order_sudo.partner_invoice_id
self._validate_transaction_kwargs(kwargs)
kwargs.update({
'reference_prefix': None, # Allow the reference to be computed based on the order
'partner_id': partner_sudo.id,
'currency_id': order_sudo.currency_id.id,
'sale_order_id': order_id, # Include the SO to allow Subscriptions tokenizing the tx
})
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
tx_sudo = self._create_transaction(
custom_create_values={'sale_order_ids': [Command.set([order_id])]}, **kwargs,
)
@@ -376,10 +376,8 @@ class PaymentPortal(payment_portal.PaymentPortal):
@http.route()
def payment_pay(self, *args, amount=None, sale_order_id=None, access_token=None, **kwargs):
""" Override of payment to replace the missing transaction values by that of the sale order.
This is necessary for the reconciliation as all transaction values, excepted the amount,
need to match exactly that of the sale order.
""" Override of `payment` to replace the missing transaction values by that of the sales
order.
:param str amount: The (possibly partial) amount to pay used to check the access token
:param str sale_order_id: The sale order for which a payment id made, as a `sale.order` id
@@ -404,7 +402,11 @@ class PaymentPortal(payment_portal.PaymentPortal):
raise ValidationError(_("The provided parameters are invalid."))
kwargs.update({
# To display on the payment form; will be later overwritten when creating the tx.
'reference': order_sudo.name,
# To fix the currency if incorrect and avoid mismatches when creating the tx.
'currency_id': order_sudo.currency_id.id,
# To fix the partner if incorrect and avoid mismatches when creating the tx.
'partner_id': order_sudo.partner_invoice_id.id,
'company_id': order_sudo.company_id.id,
'sale_order_id': sale_order_id,
@@ -412,38 +414,25 @@ class PaymentPortal(payment_portal.PaymentPortal):
return super().payment_pay(*args, amount=amount, access_token=access_token, **kwargs)
def _get_extra_payment_form_values(self, sale_order_id=None, **kwargs):
""" Override of `payment` to add the sale order id to the payment form values.
""" Override of `payment` to reroute the payment flow to the portal view of the sales order.
:param int sale_order_id: The sale order for which a payment id made, as a `sale.order` id
:return: The extended rendering context values
:param str sale_order_id: The sale order for which a payment is made, as a `sale.order` id.
:return: The extended rendering context values.
:rtype: dict
"""
form_values = super()._get_extra_payment_form_values(sale_order_id=sale_order_id, **kwargs)
if sale_order_id:
form_values['sale_order_id'] = sale_order_id
sale_order_id = self._cast_as_int(sale_order_id)
order_sudo = request.env['sale.order'].sudo().browse(sale_order_id)
# Interrupt the payment flow if the sales order has been canceled.
order_sudo = request.env['sale.order'].sudo().browse(sale_order_id)
if order_sudo.state == 'cancel':
form_values['amount'] = 0.0
# Reroute the next steps of the payment flow to the portal view of the sales order.
form_values.update({
'transaction_route': order_sudo.get_portal_url(suffix='/transaction'),
'landing_route': order_sudo.get_portal_url(),
'access_token': order_sudo.access_token,
})
return form_values
def _create_transaction(self, *args, sale_order_id=None, custom_create_values=None, **kwargs):
""" Override of payment to add the sale order id in the custom create values.
:param int sale_order_id: The sale order for which a payment id made, as a `sale.order` id
:param dict custom_create_values: Additional create values overwriting the default ones
:return: The result of the parent method
:rtype: recordset of `payment.transaction`
"""
if sale_order_id:
if custom_create_values is None:
custom_create_values = {}
# As this override is also called if the flow is initiated from sale or website_sale, we
# must avoid overriding whatever value these modules could have already set.
if 'sale_order_ids' not in custom_create_values: # We are in the payment module's flow
custom_create_values['sale_order_ids'] = [Command.set([int(sale_order_id)])]
return super()._create_transaction(
*args, sale_order_id=sale_order_id, custom_create_values=custom_create_values, **kwargs
)
-1
View File
@@ -1569,7 +1569,6 @@ class SaleOrder(models.Model):
amount = amount_max
return {
'description': self.name,
'currency_id': self.currency_id.id,
'partner_id': self.partner_invoice_id.id,
'amount': amount,
-25
View File
@@ -1,25 +0,0 @@
/** @odoo-module **/
import paymentForm from '@payment/js/payment_form';
paymentForm.include({
// #=== PAYMENT FLOW ===#
/**
* Add `sale_order_id` to the params for the RPC to the transaction route.
*
* @override method from @payment/js/payment_form
* @private
* @return {object} The extended transaction route params.
*/
_prepareTransactionRouteParams() {
const transactionRouteParams = this._super(...arguments);
return {
...transactionRouteParams,
'sale_order_id': this.paymentContext['saleOrderId']
? parseInt(this.paymentContext['saleOrderId']) : undefined,
};
},
});
+49 -43
View File
@@ -2,7 +2,7 @@
from unittest.mock import ANY, patch
from odoo.fields import Command
from odoo.tests import tagged
from odoo.tests import tagged, JsonRpcException
from odoo.tools import mute_logger
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
@@ -37,23 +37,22 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
self.assertEqual(tx_context['amount'], self.sale_order.amount_total)
self.assertEqual(tx_context['sale_order_id'], self.sale_order.id)
route_values.update({
# /my/orders/<id>/transaction/
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': tx_context['amount'],
'flow': 'direct',
'tokenization_requested': False,
'validation_route': False,
'reference_prefix': None, # Force empty prefix to fallback on SO reference
'landing_route': tx_context['landing_route'],
'amount': tx_context['amount'],
'currency_id': tx_context['currency_id'],
})
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(**route_values)
processing_values = self._get_processing_values(
tx_route=tx_context['transaction_route'], **tx_route_values
)
tx_sudo = self._get_tx(processing_values['reference'])
self.assertEqual(tx_sudo.sale_order_ids, self.sale_order)
@@ -87,29 +86,29 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
# test customized /payment/pay route with sale_order_id param
# partial amount specified
self.amount = self.sale_order.amount_total / 2.0
route_values = self._prepare_pay_values()
route_values['sale_order_id'] = self.sale_order.id
pay_route_values = self._prepare_pay_values()
pay_route_values['sale_order_id'] = self.sale_order.id
tx_context = self._get_portal_pay_context(**route_values)
tx_context = self._get_portal_pay_context(**pay_route_values)
self.assertEqual(tx_context['reference_prefix'], self.reference)
self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
self.assertEqual(tx_context['amount'], self.amount)
self.assertEqual(tx_context['sale_order_id'], self.sale_order.id)
route_values.update({
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': tx_context['amount'],
'flow': 'direct',
'tokenization_requested': False,
'validation_route': False,
'reference_prefix': tx_context['reference_prefix'],
'landing_route': tx_context['landing_route'],
})
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(**route_values)
processing_values = self._get_processing_values(
tx_route=tx_context['transaction_route'], **tx_route_values
)
tx_sudo = self._get_tx(processing_values['reference'])
self.assertEqual(tx_sudo.sale_order_ids, self.sale_order)
@@ -117,7 +116,6 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
self.assertEqual(tx_sudo.partner_id, self.sale_order.partner_invoice_id)
self.assertEqual(tx_sudo.company_id, self.sale_order.company_id)
self.assertEqual(tx_sudo.currency_id, self.sale_order.currency_id)
self.assertEqual(tx_sudo.reference, self.reference)
self.assertEqual(tx_sudo.sale_order_ids.transaction_ids, tx_sudo)
tx_sudo._set_done()
@@ -126,29 +124,29 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
self.assertEqual(self.sale_order.state, 'draft') # Only a partial amount was paid
# Pay the remaining amount
route_values = self._prepare_pay_values()
route_values['sale_order_id'] = self.sale_order.id
pay_route_values = self._prepare_pay_values()
pay_route_values['sale_order_id'] = self.sale_order.id
tx_context = self._get_portal_pay_context(**route_values)
tx_context = self._get_portal_pay_context(**pay_route_values)
self.assertEqual(tx_context['reference_prefix'], self.reference)
self.assertEqual(tx_context['currency_id'], self.sale_order.currency_id.id)
self.assertEqual(tx_context['partner_id'], self.sale_order.partner_invoice_id.id)
self.assertEqual(tx_context['amount'], self.amount)
self.assertEqual(tx_context['sale_order_id'], self.sale_order.id)
route_values.update({
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': tx_context['amount'],
'flow': 'direct',
'tokenization_requested': False,
'validation_route': False,
'reference_prefix': tx_context['reference_prefix'],
'landing_route': tx_context['landing_route'],
})
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(**route_values)
processing_values = self._get_processing_values(
tx_route=tx_context['transaction_route'], **tx_route_values
)
tx2_sudo = self._get_tx(processing_values['reference'])
self.assertEqual(tx2_sudo.sale_order_ids, self.sale_order)
@@ -157,10 +155,6 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
self.assertEqual(tx2_sudo.company_id, self.sale_order.company_id)
self.assertEqual(tx2_sudo.currency_id, self.sale_order.currency_id)
# We are paying a second time with the same reference (prefix)
# a suffix is added to respect unique reference constraint
reference = self.reference + "-1"
self.assertEqual(tx2_sudo.reference, reference)
self.assertEqual(self.sale_order.state, 'draft')
self.assertEqual(self.sale_order.transaction_ids, tx_sudo + tx2_sudo)
@@ -173,23 +167,25 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
self.product.invoice_policy = 'delivery'
self.amount = self.sale_order.amount_total / 2.0
route_values = self._prepare_pay_values()
route_values['sale_order_id'] = self.sale_order.id
pay_route_values = self._prepare_pay_values()
pay_route_values['sale_order_id'] = self.sale_order.id
tx_context = self._get_portal_pay_context(**route_values)
tx_context = self._get_portal_pay_context(**pay_route_values)
route_values.update({
tx_route_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': tx_context['amount'],
'flow': 'direct',
'tokenization_requested': False,
'validation_route': False,
'reference_prefix': tx_context['reference_prefix'],
'landing_route': tx_context['landing_route'],
})
'access_token': tx_context['access_token'],
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(**route_values)
processing_values = self._get_processing_values(
tx_route=tx_context['transaction_route'], **tx_route_values
)
tx_sudo = self._get_tx(processing_values['reference'])
tx_sudo._set_done()
@@ -388,3 +384,13 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
invoice = self.sale_order.invoice_ids
self.assertTrue(len(invoice) == 1)
self.assertTrue(invoice.line_ids[0].is_downpayment)
@mute_logger('odoo.http')
def test_transaction_route_rejects_unexpected_kwarg(self):
url = self._build_url(f'/my/orders/{self.sale_order.id}/transaction')
route_kwargs = {
'access_token': self.sale_order._portal_ensure_token(),
'partner_id': self.partner.id, # This should be rejected.
}
with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
self.make_jsonrpc_request(url, route_kwargs)
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Include sale-related values to the payment form to pass them to the client. -->
<template id="payment_form_inherit" inherit_id="payment.form">
<xpath expr="//form[@id='o_payment_form']" position="attributes">
<attribute name="t-att-data-sale-order-id">sale_order_id</attribute>
</xpath>
</template>
</odoo>
+3 -5
View File
@@ -51,13 +51,11 @@ class PaymentPortal(payment_portal.PaymentPortal):
else:
partner_id = request.env.user.partner_id.id
# Don't allow passing arbitrary create values and avoid tokenization for
# the public user.
self._validate_transaction_kwargs(kwargs, additional_allowed_keys=(
'donation_comment', 'donation_recipient_email', 'partner_details', 'reference_prefix'
))
if use_public_partner:
kwargs['custom_create_values'] = {'tokenize': False}
else:
kwargs.pop('custom_create_values', None)
tx_sudo = self._create_transaction(
amount=amount, currency_id=currency_id, partner_id=partner_id, **kwargs
)
@@ -105,6 +105,10 @@ PaymentForm.include({
const transactionRouteParams = this._super(...arguments);
return $('.o_donation_payment_form').length ? {
...transactionRouteParams,
'partner_id': parseInt(this.paymentContext['partnerId']),
'currency_id': this.paymentContext['currencyId']
? parseInt(this.paymentContext['currencyId']) : null,
'reference_prefix':this.paymentContext['referencePrefix']?.toString(),
'partner_details': {
'name': this.$('input[name="name"]').val(),
'email': this.$('input[name="email"]').val(),
+2 -2
View File
@@ -1861,12 +1861,12 @@ class PaymentPortal(payment_portal.PaymentPortal):
order_sudo._check_cart_is_ready_to_be_paid()
self._validate_transaction_kwargs(kwargs)
kwargs.update({
'reference_prefix': None, # Allow the reference to be computed based on the order
'partner_id': order_sudo.partner_invoice_id.id,
'currency_id': order_sudo.currency_id.id,
'sale_order_id': order_id, # Include the SO to allow Subscriptions to tokenize the tx
})
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
if not kwargs.get('amount'):
kwargs['amount'] = order_sudo.amount_total
@@ -1,14 +1,15 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.models import Command
from odoo.tests.common import tagged
from odoo.tests.common import tagged, JsonRpcException
from odoo.tools import mute_logger
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.website.tools import MockRequest
@tagged('post_install', '-at_install')
class WebsiteSaleCartPayment(PaymentCommon):
class WebsiteSaleCartPayment(PaymentHttpCommon):
@classmethod
def setUpClass(cls):
@@ -53,3 +54,13 @@ class WebsiteSaleCartPayment(PaymentCommon):
msg=f"The transaction state '{paid_order_tx_state}' should prevent retrieving "
f"the linked order.",
)
@mute_logger('odoo.http')
def test_transaction_route_rejects_unexpected_kwarg(self):
url = self._build_url(f'/shop/payment/transaction/{self.order.id}')
route_kwargs = {
'access_token': self.order._portal_ensure_token(),
'partner_id': self.partner.id, # This should be rejected.
}
with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
self.make_jsonrpc_request(url, route_kwargs)