[ADD] website_sale_disable_cart: extract cart restriction from website_sale_aplicoop
The "shop as a read-only catalog" behaviour lived inside website_sale_aplicoop, so every site that wanted the eskaera flow also lost the standard cart. It now ships as its own installable addon, with the redirect target configurable instead of hardcoded to /eskaera. - website_sale_disable_cart: hides the cart UI (12 header styles + the product card quick-add) and overrides the standard cart endpoints. Redirect URL is configurable in Website settings (default /shop); only site-internal paths are accepted, so a misconfigured value cannot turn the shop into an open redirect nor loop back into a disabled route. - Fixes carried over from the original code: /shop/cart/quantity is the Odoo 18 path (it was /shop/cart_quantity, which never matched), the boxed, sidebar and sales two/three/four headers were not covered (the cart link stayed visible on them), and the routes now override the standard methods instead of registering duplicate ones. - website_sale_aplicoop 18.0.1.12.0: drops the view file and the four redirect routes; installing it no longer touches the standard shop. Upgrade order matters: update website_sale_aplicoop first, then install website_sale_disable_cart in a second Odoo run — both use the same XPaths and obsolete records are only cleaned up at the end of a run. Tests: 8/8 in website_sale_disable_cart, aplicoop unaffected (its 2 failures predate this change). Verified live on a DB clone: /shop/cart returns 303 to the configured URL and no cart markup remains on /shop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
73660ee226
commit
f2194c5367
28 changed files with 733 additions and 91 deletions
1
website_sale_disable_cart/tests/__init__.py
Normal file
1
website_sale_disable_cart/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_disable_cart
|
||||
103
website_sale_disable_cart/tests/test_disable_cart.py
Normal file
103
website_sale_disable_cart/tests/test_disable_cart.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
import json
|
||||
|
||||
from odoo import http
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import HttpCase
|
||||
|
||||
REDIRECT_URL_PARAM = "website_sale_disable_cart.redirect_url"
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestDisableCart(HttpCase):
|
||||
"""Check the standard cart endpoints are neutralized."""
|
||||
|
||||
def _set_redirect_url(self, url):
|
||||
self.env["ir.config_parameter"].sudo().set_param(REDIRECT_URL_PARAM, url)
|
||||
|
||||
def _location_of(self, path):
|
||||
"""Return the redirect target of a GET on ``path``."""
|
||||
response = self.url_open(path, allow_redirects=False)
|
||||
self.assertIn(response.status_code, (301, 302, 303, 307, 308))
|
||||
return response.headers.get("Location", "")
|
||||
|
||||
def test_cart_redirects_to_default_url(self):
|
||||
"""Without configuration, /shop/cart falls back to /shop."""
|
||||
self.env["ir.config_parameter"].sudo().search(
|
||||
[("key", "=", REDIRECT_URL_PARAM)]
|
||||
).unlink()
|
||||
self.assertTrue(self._location_of("/shop/cart").endswith("/shop"))
|
||||
|
||||
def test_cart_redirects_to_configured_url(self):
|
||||
"""The configured internal path is honored."""
|
||||
self._set_redirect_url("/eskaera")
|
||||
self.assertTrue(self._location_of("/shop/cart").endswith("/eskaera"))
|
||||
|
||||
def test_external_redirect_url_is_rejected(self):
|
||||
"""An external URL must not be used (no open redirect)."""
|
||||
self._set_redirect_url("https://example.com/phishing")
|
||||
self.assertTrue(self._location_of("/shop/cart").endswith("/shop"))
|
||||
|
||||
def test_protocol_relative_redirect_url_is_rejected(self):
|
||||
"""A protocol relative URL is external too."""
|
||||
self._set_redirect_url("//example.com/phishing")
|
||||
self.assertTrue(self._location_of("/shop/cart").endswith("/shop"))
|
||||
|
||||
def test_cart_redirect_url_cannot_loop(self):
|
||||
"""Pointing the redirect back to the cart must not loop."""
|
||||
self._set_redirect_url("/shop/cart")
|
||||
self.assertTrue(self._location_of("/shop/cart").endswith("/shop"))
|
||||
|
||||
def _json_rpc(self, path, params=None):
|
||||
"""Call a JSON route and return its result."""
|
||||
response = self.url_open(
|
||||
path,
|
||||
data=json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "call", "params": params or {}}
|
||||
),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
payload = response.json()
|
||||
self.assertNotIn("error", payload, payload.get("error"))
|
||||
return payload["result"]
|
||||
|
||||
def test_cart_update_redirects_without_creating_an_order(self):
|
||||
"""POSTing to /shop/cart/update redirects and leaves no order behind."""
|
||||
self._set_redirect_url("/shop")
|
||||
product = self.env["product.product"].create(
|
||||
{"name": "Disabled Cart Product", "list_price": 10.0, "is_published": True}
|
||||
)
|
||||
orders_before = self.env["sale.order"].search_count([])
|
||||
# A public session is needed to build a valid CSRF token, otherwise the
|
||||
# request is rejected before reaching the controller.
|
||||
self.authenticate(None, None)
|
||||
response = self.url_open(
|
||||
"/shop/cart/update",
|
||||
data={
|
||||
"product_id": product.id,
|
||||
"add_qty": 1,
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
self.assertIn(response.status_code, (302, 303))
|
||||
self.assertTrue(response.headers.get("Location", "").endswith("/shop"))
|
||||
self.assertEqual(self.env["sale.order"].search_count([]), orders_before)
|
||||
|
||||
def test_cart_update_json_is_a_noop(self):
|
||||
"""The JSON update endpoint answers an empty payload and writes nothing."""
|
||||
product = self.env["product.product"].create(
|
||||
{"name": "Disabled Cart Product", "list_price": 10.0, "is_published": True}
|
||||
)
|
||||
orders_before = self.env["sale.order"].search_count([])
|
||||
result = self._json_rpc(
|
||||
"/shop/cart/update_json", {"product_id": product.id, "add_qty": 1}
|
||||
)
|
||||
self.assertEqual(result, {})
|
||||
self.assertEqual(self.env["sale.order"].search_count([]), orders_before)
|
||||
|
||||
def test_cart_quantity_is_zero(self):
|
||||
"""The JSON quantity endpoint always answers 0."""
|
||||
self.assertEqual(self._json_rpc("/shop/cart/quantity"), 0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue