diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index b243a5b52e2..56261bb3294 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -1524,6 +1524,12 @@ class WebsiteSale(http.Controller): """ if sale_order_id is None: order = request.website.sale_get_order() + if not order and 'sale_last_order_id' in request.session: + # Retrieve the last known order from the session if the session key `sale_order_id` + # was prematurely cleared. This is done to prevent the user from updating their cart + # after payment in case they don't return from payment through this route. + last_order_id = request.session['sale_last_order_id'] + order = request.env['sale.order'].sudo().browse(last_order_id).exists() else: order = request.env['sale.order'].sudo().browse(sale_order_id) assert order.id == request.session.get('sale_last_order_id') diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index 387751c76b5..2af5d5e8c7b 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -333,6 +333,13 @@ class Website(models.Model): else: sale_order_sudo = SaleOrder + # Ignore the current order if a payment has been initiated. We don't want to retrieve the + # cart and allow the user to update it when the payment is about to confirm it. + if sale_order_sudo and sale_order_sudo.get_portal_last_transaction().state in ( + 'pending', 'authorized', 'done' + ): + sale_order_sudo = None + if not (sale_order_sudo or force_create): # Do not create a SO record unless needed if request.session.get('sale_order_id'): diff --git a/addons/website_sale/tests/__init__.py b/addons/website_sale/tests/__init__.py index f5b59d1a2a0..c7cbc0fc02d 100644 --- a/addons/website_sale/tests/__init__.py +++ b/addons/website_sale/tests/__init__.py @@ -7,6 +7,7 @@ from . import test_sale_process from . import test_sitemap from . import test_website_sale_add_to_cart_snippet from . import test_website_sale_cart_abandoned +from . import test_website_sale_cart_payment from . import test_website_sale_cart_popover from . import test_website_sale_cart_recovery from . import test_website_sale_cart diff --git a/addons/website_sale/tests/test_website_sale_cart_payment.py b/addons/website_sale/tests/test_website_sale_cart_payment.py new file mode 100644 index 00000000000..34ea47f6930 --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_cart_payment.py @@ -0,0 +1,54 @@ +# 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.addons.payment.tests.common import PaymentCommon +from odoo.addons.website.tools import MockRequest + + +@tagged('post_install', '-at_install') +class WebsiteSaleCartPayment(PaymentCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.website = cls.env['website'].get_current_website() + with MockRequest(cls.env, website=cls.website): + cls.order = cls.website.sale_get_order(force_create=True) # Create the cart to retrieve + cls.tx = cls.env['payment.transaction'].create({ + 'amount': cls.amount, + 'currency_id': cls.currency.id, + 'provider_id': cls.provider.id, + 'reference': cls.reference, + 'operation': 'online_redirect', + 'partner_id': cls.partner.id, + }) + cls.order.write({'transaction_ids': [Command.set([cls.tx.id])]}) + + def test_unpaid_orders_can_be_retrieved(self): + """ Test that fetching sales orders linked to a payment transaction in the states 'draft', + 'cancel', or 'error' returns the orders. """ + for unpaid_order_tx_state in ('draft', 'cancel', 'error'): + self.tx.state = unpaid_order_tx_state + with MockRequest(self.env, website=self.website, sale_order_id=self.order.id): + self.assertEqual( + self.website.sale_get_order(), + self.order, + msg=f"The transaction state '{unpaid_order_tx_state}' should not prevent " + f"retrieving the linked order.", + ) + + def test_paid_orders_cannot_be_retrieved(self): + """ Test that fetching sales orders linked to a payment transaction in the states 'pending', + 'authorized', or 'done' returns an empty recordset to prevent updating the paid orders. """ + self.tx.provider_id.support_manual_capture = True + for paid_order_tx_state in ('pending', 'authorized', 'done'): + self.tx.state = paid_order_tx_state + with MockRequest(self.env, website=self.website, sale_order_id=self.order.id): + self.assertFalse( + self.website.sale_get_order(), + msg=f"The transaction state '{paid_order_tx_state}' should prevent retrieving " + f"the linked order.", + ) diff --git a/addons/website_sale_delivery/tests/test_controller.py b/addons/website_sale_delivery/tests/test_controller.py index 91fc865a72b..cdee223196e 100644 --- a/addons/website_sale_delivery/tests/test_controller.py +++ b/addons/website_sale_delivery/tests/test_controller.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + +from unittest.mock import patch + from odoo.exceptions import UserError from odoo.addons.payment.tests.common import PaymentCommon from odoo.addons.website_sale_delivery.controllers.main import WebsiteSaleDelivery @@ -19,7 +22,11 @@ class TestWebsiteSaleDeliveryController(PaymentCommon): order = self.website.sale_get_order(force_create=True) order.transaction_ids = self._create_transaction(flow='redirect', state='pending') with self.assertRaises(UserError): - self.Controller.update_eshop_carrier(carrier_id=1) + with patch( + 'odoo.addons.website_sale.models.website.Website.sale_get_order', + return_value=order, + ): # Patch to retrieve the order even if it is linked to a pending transaction. + self.Controller.update_eshop_carrier(carrier_id=1) # test that changing the carrier while there is a draft transaction doesn't raise an error def test_controller_change_carrier_when_draft_transaction(self):