From f2194c5367b5d6a47d6d778b4f3a7c5dc83f4c63 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Thu, 6 Aug 2026 11:34:59 +0200 Subject: [PATCH] [ADD] website_sale_disable_cart: extract cart restriction from website_sale_aplicoop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/copilot-instructions.md | 2 + CLAUDE.md | 2 + website_sale_aplicoop/CHANGELOG.md | 19 ++++ website_sale_aplicoop/README.rst | 52 +++------ website_sale_aplicoop/README_DEV.md | 7 +- website_sale_aplicoop/__manifest__.py | 3 +- .../controllers/website_sale.py | 47 -------- website_sale_disable_cart/LICENSE.txt | 56 +++++++++ website_sale_disable_cart/README.rst | 106 ++++++++++++++++++ website_sale_disable_cart/__init__.py | 2 + website_sale_disable_cart/__manifest__.py | 22 ++++ .../controllers/__init__.py | 1 + .../controllers/website_sale.py | 102 +++++++++++++++++ website_sale_disable_cart/i18n/es.po | 52 +++++++++ website_sale_disable_cart/i18n/eu.po | 52 +++++++++ .../i18n/website_sale_disable_cart.pot | 47 ++++++++ website_sale_disable_cart/models/__init__.py | 1 + .../models/res_config_settings.py | 18 +++ .../readme/CONFIGURE.rst | 15 +++ .../readme/CONTRIBUTORS.rst | 6 + website_sale_disable_cart/readme/CREDITS.rst | 9 ++ .../readme/DESCRIPTION.rst | 25 +++++ website_sale_disable_cart/readme/INSTALL.rst | 13 +++ website_sale_disable_cart/readme/USAGE.rst | 10 ++ website_sale_disable_cart/tests/__init__.py | 1 + .../tests/test_disable_cart.py | 103 +++++++++++++++++ .../views/res_config_settings_views.xml | 29 +++++ .../website_sale_disable_cart_templates.xml | 22 +++- 28 files changed, 733 insertions(+), 91 deletions(-) create mode 100644 website_sale_disable_cart/LICENSE.txt create mode 100644 website_sale_disable_cart/README.rst create mode 100644 website_sale_disable_cart/__init__.py create mode 100644 website_sale_disable_cart/__manifest__.py create mode 100644 website_sale_disable_cart/controllers/__init__.py create mode 100644 website_sale_disable_cart/controllers/website_sale.py create mode 100644 website_sale_disable_cart/i18n/es.po create mode 100644 website_sale_disable_cart/i18n/eu.po create mode 100644 website_sale_disable_cart/i18n/website_sale_disable_cart.pot create mode 100644 website_sale_disable_cart/models/__init__.py create mode 100644 website_sale_disable_cart/models/res_config_settings.py create mode 100644 website_sale_disable_cart/readme/CONFIGURE.rst create mode 100644 website_sale_disable_cart/readme/CONTRIBUTORS.rst create mode 100644 website_sale_disable_cart/readme/CREDITS.rst create mode 100644 website_sale_disable_cart/readme/DESCRIPTION.rst create mode 100644 website_sale_disable_cart/readme/INSTALL.rst create mode 100644 website_sale_disable_cart/readme/USAGE.rst create mode 100644 website_sale_disable_cart/tests/__init__.py create mode 100644 website_sale_disable_cart/tests/test_disable_cart.py create mode 100644 website_sale_disable_cart/views/res_config_settings_views.xml rename website_sale_aplicoop/views/website_sale_disable_cart.xml => website_sale_disable_cart/views/website_sale_disable_cart_templates.xml (71%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1a7b93b..e54e4d8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -182,6 +182,7 @@ addons-cm/ ├── account_invoice_triple_discount_readonly/ # Fix bug acumulación triple descuento ├── # --- Ventas y web --- ├── website_sale_aplicoop/ # Sistema eskaera (compras grupo) +├── website_sale_disable_cart/ # Tienda solo catálogo: desactiva el carrito estándar ├── portal_event_registration/ # Portal: ver registros de eventos + adjuntos ├── # --- Membresías --- ├── membership_monthly_invoicing/ # Factura mensual de membresía por socio (cron) @@ -220,6 +221,7 @@ addons-cm/ **Ventas y web** - [website_sale_aplicoop](../website_sale_aplicoop/README.rst) - Sistema eskaera completo +- [website_sale_disable_cart](../website_sale_disable_cart/README.rst) - Tienda solo catálogo: oculta el carrito estándar y redirige `/shop/cart*` - [portal_event_registration](../portal_event_registration/README.rst) - Portal: registros de eventos + adjuntos al chatter **Membresías** diff --git a/CLAUDE.md b/CLAUDE.md index c6c246f..a966f9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,6 +58,8 @@ Sales & website: - `website_sale_aplicoop` — Eskaera: collaborative purchasing for consumer co-ops (group orders, per-member carts, cutoff/pickup dates, lazy loading, multi-language). +- `website_sale_disable_cart` — turns `/shop` into a read-only catalog: hides the cart UI and + redirects the standard `/shop/cart*` routes to a configurable URL (default `/shop`). - `portal_event_registration` — portal users view their event registrations + upload attachments to chatter. Membership: diff --git a/website_sale_aplicoop/CHANGELOG.md b/website_sale_aplicoop/CHANGELOG.md index 5972f82..1e1e3fe 100644 --- a/website_sale_aplicoop/CHANGELOG.md +++ b/website_sale_aplicoop/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog - Website Sale Aplicoop +## [18.0.1.12.0] - 2026-08-06 + +### Removed + +- **Standard cart restriction extracted to `website_sale_disable_cart`**: the cart + UI removal (header cart link, add-to-cart buttons) and the `/shop/cart*` route + redirects no longer live in this module, so installing Aplicoop leaves the + standard `website_sale` shop untouched. Sites that want the previous behaviour + must install `website_sale_disable_cart` and set its *Cart Redirect URL* to + `/eskaera`. The new module also fixes the route paths (Odoo 18 uses + `/shop/cart/quantity`, not `/shop/cart_quantity`), covers the 5 header styles + that were missing (boxed, sidebar, sales two/three/four — the header cart link + was still visible on those) and overrides the standard endpoints instead of + registering duplicate ones. + Upgrade order matters: update this module **first** (so its old templates are + dropped) and install `website_sale_disable_cart` in a **second** Odoo run — + both sets of templates use the same XPaths and obsolete records are only + cleaned up at the end of a run, so a single combined run fails. + ## [18.0.1.11.0] - 2026-07-15 ### Added diff --git a/website_sale_aplicoop/README.rst b/website_sale_aplicoop/README.rst index bf621c1..a73a6a3 100644 --- a/website_sale_aplicoop/README.rst +++ b/website_sale_aplicoop/README.rst @@ -142,48 +142,22 @@ This module was inspired by the original **Aplicoop** project: The original Aplicoop project served as a pioneering solution for collaborative consumption group orders, and this module brings its functionality to the modern Odoo platform. -Notes - Shop behaves as a simple catalog -========================================= +Notes - Standard shop and cart +============================== -Starting with the recent update, this module converts the default Odoo -``/shop`` storefront into a simple product catalog (no standard ``website_sale`` -shopping cart). The change is intentional for sites that use the Aplicoop -"eskaera" flow as the single shopping experience. +This module adds the "eskaera" group order flow (``/eskaera``); it no longer +touches the standard ``website_sale`` storefront. The default ``/shop`` and its +shopping cart keep working as usual after installing it. -What the module does ---------------------- - -- Hides the standard header cart link and badge. -- Removes the "Add to cart" quick-add area from product listings. -- Redirects standard cart endpoints to the group-order flow (``/eskaera``): - ``/shop/cart``, ``/shop/cart/update``, ``/shop/cart/update_json``, ``/shop/cart_quantity``. - -Files involved --------------- - -- ``views/website_sale_disable_cart.xml`` — templates that hide/remove cart UI -- ``controllers/website_sale.py`` — routes that redirect cart endpoints to ``/eskaera`` -- ``__manifest__.py`` — includes the new view file - -How to apply or revert ------------------------ - -To apply the change (already applied when the module is installed/updated): +Sites that want the standard cart disabled — so that the group order flow is +the single shopping experience — should additionally install +``website_sale_disable_cart``, which hides the cart UI and redirects the +standard cart endpoints to a configurable URL. That behaviour used to live in +this module (up to 18.0.1.11.0) and was extracted in 18.0.1.12.0. :: - docker-compose run --rm odoo odoo -d odoo -u website_sale_aplicoop --stop-after-init - docker-compose up -d + docker-compose run --rm odoo odoo -d odoo -i website_sale_disable_cart --stop-after-init -To revert back to the standard ``website_sale`` behaviour: - -1. Remove ``views/website_sale_disable_cart.xml`` from the ``data`` section in - ``__manifest__.py``. -2. Update the module: - -:: - - docker-compose run --rm odoo odoo -d odoo -u website_sale_aplicoop --stop-after-init - -Note: Reverting may expose standard cart UI and routes; ensure your site -content and workflows are adapted accordingly. +Then set *Website > Configuration > Settings > Disabled Cart > Cart Redirect +URL* to ``/eskaera``. diff --git a/website_sale_aplicoop/README_DEV.md b/website_sale_aplicoop/README_DEV.md index 650289c..32c5c62 100644 --- a/website_sale_aplicoop/README_DEV.md +++ b/website_sale_aplicoop/README_DEV.md @@ -7,7 +7,8 @@ Sistema de pedidos colaborativos para grupos de consumo. Reemplaza el legacy Apl ## Resumen de funcionalidades - Gestión completa de órdenes de grupo (draft → confirmed → collected → invoiced → completed) -- Carrito separado por grupo de consumo; tienda estándar de `website_sale` deshabilitada +- Carrito separado por grupo de consumo (la tienda estándar de `website_sale` se + puede deshabilitar aparte con `website_sale_disable_cart`) - Control de catálogo por listas de inclusión (proveedores, categorías, productos) y exclusión (blacklists) - Cron diario de auto-confirmación + creación automática de lotes de picking - Paginación de productos (lazy loading) para cargas rápidas @@ -86,7 +87,9 @@ Todos los endpoints viven bajo `/eskaera/`: | `POST /eskaera//confirm` | Confirmar carrito (sale.order en draft) | | `POST /eskaera/clear-cart` | Limpiar carrito actual | -Las rutas `/cart` y `/shop` de `website_sale` están redirigidas a `/eskaera`. +Las rutas estándar de `website_sale` (`/shop` y su carrito) no se tocan desde +este módulo. Para desactivar el carrito estándar y redirigirlo a `/eskaera`, +instala `website_sale_disable_cart` y configura la URL de redirección. **Seguridad portal:** los usuarios solo ven órdenes de su `consumer_group_id`. La regla `rule_group_order_company_read` incluye guardia `user.share`. diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py index 0140a35..5baedf4 100644 --- a/website_sale_aplicoop/__manifest__.py +++ b/website_sale_aplicoop/__manifest__.py @@ -3,7 +3,7 @@ { # noqa: B018 "name": "Website Sale - Aplicoop", - "version": "18.0.1.11.0", + "version": "18.0.1.12.0", "category": "Website/Sale", "summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders", "author": "Odoo Community Association (OCA), Criptomart", @@ -39,7 +39,6 @@ "views/res_partner_views.xml", "views/res_config_settings_views.xml", "views/website_templates.xml", - "views/website_sale_disable_cart.xml", "views/product_template_views.xml", "views/sale_order_views.xml", "views/stock_picking_views.xml", diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index fad2dbf..8de6551 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -2457,50 +2457,3 @@ class AplicoopWebsiteSale(WebsiteSale): "empty_cart": "Your cart is empty", "added_to_cart": "added to cart", } - - # ================================================================ - # CART REDIRECT METHODS - Redirect /shop/cart routes to /eskaera - # ================================================================ - - @http.route(["/shop/cart"], type="http", auth="public", website=True) - def cart_redirect(self, access_token=None, revive="", **post): - """Redirect /shop/cart to /eskaera (no standard cart).""" - _logger.info("🛒 Redirecting /shop/cart → /eskaera") - return http.redirect_with_hash("/eskaera") - - @http.route( - ["/shop/cart/update"], - type="http", - auth="public", - website=True, - methods=["POST"], - ) - def cart_update_redirect(self, **post): - """Redirect /shop/cart/update to /eskaera (no standard cart).""" - _logger.info("🛒 Redirecting /shop/cart/update → /eskaera") - return http.redirect_with_hash("/eskaera") - - @http.route( - ["/shop/cart/update_json"], - type="http", - auth="public", - website=True, - methods=["POST"], - csrf=False, - ) - def cart_update_json_redirect(self, **post): - """Redirect /shop/cart/update_json to /eskaera (no standard cart).""" - _logger.info("🛒 Redirecting /shop/cart/update_json → /eskaera") - return http.redirect_with_hash("/eskaera") - - @http.route( - ["/shop/cart_quantity"], - type="http", - auth="public", - website=True, - methods=["GET"], - ) - def cart_quantity_redirect(self): - """Redirect /shop/cart_quantity to /eskaera (no standard cart).""" - _logger.info("🛒 Redirecting /shop/cart_quantity → /eskaera") - return http.redirect_with_hash("/eskaera") diff --git a/website_sale_disable_cart/LICENSE.txt b/website_sale_disable_cart/LICENSE.txt new file mode 100644 index 0000000..61a65ee --- /dev/null +++ b/website_sale_disable_cart/LICENSE.txt @@ -0,0 +1,56 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Website Sale - Aplicoop +Copyright 2025 Criptomart SL + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +--- + +FULL LICENSE TEXT +================= + +For the complete AGPL-3 license text, see: +https://www.gnu.org/licenses/agpl-3.0.html + +--- + +SUMMARY OF RIGHTS +================= + +When you distribute a modified version under AGPL-3, you must: + +1. Keep the same license (AGPL-3) +2. Provide a copy of the license with your distribution +3. State what changes you made +4. Include the original copyright notices +5. If distributed over a network, provide source code access + +Detailed information: https://www.gnu.org/licenses/agpl-3.0-standalone.html + +--- + +ATTRIBUTION +=========== + +This module was developed by: Criptomart SL +Website: https://criptomart.net + +Original inspiration: Aplicoop project +https://sourceforge.net/projects/aplicoop/ + +--- + +This file is part of the Website Sale - Aplicoop module for Odoo. diff --git a/website_sale_disable_cart/README.rst b/website_sale_disable_cart/README.rst new file mode 100644 index 0000000..c1a3c6b --- /dev/null +++ b/website_sale_disable_cart/README.rst @@ -0,0 +1,106 @@ +=============================== +Website Sale - Disable Cart +=============================== + +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg + :target: https://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. image:: https://img.shields.io/badge/Odoo-18.0-blue + :alt: Odoo: 18.0 + +Turn the standard Odoo eCommerce shop into a **read-only product catalog**: +visitors can browse, search and open product pages, but the standard +``website_sale`` shopping cart is not reachable any more. + +It is meant for sites where the actual ordering happens somewhere else — for +example the group order flow of ``website_sale_aplicoop`` (``/eskaera``), a +quotation request form, or an offline/phone process — while the public shop is +still useful as a catalog. + +What the module does +==================== + +- Hides the cart link and badge from all 12 standard website header styles + (default, mobile, hamburger, stretch, vertical, search, boxed, sidebar and + sales one to four). +- Removes the "Add to cart" quick-add area from the product listing cards. +- Neutralizes the standard cart endpoints: + + - ``/shop/cart`` and ``/shop/cart/update`` redirect to a configurable URL. + - ``/shop/cart/update_json`` returns an empty payload (no cart is written). + - ``/shop/cart/quantity`` always returns ``0``. + - ``/shop/cart/clear`` does nothing. + +Only the **cart** is disabled. Product listing, search, product pages and the +rest of ``website_sale`` are untouched. Checkout and payment routes are left +untouched too; with an empty cart they redirect back to the cart route, and +therefore to the configured URL. + +Configuration +============= + +Go to *Website > Configuration > Settings > Disabled Cart* and set +**Cart Redirect URL**: the internal path visitors land on when they reach a +standard cart URL. + +- Default: ``/shop``. +- Stored in the system parameter ``website_sale_disable_cart.redirect_url``. +- Only site-internal paths are accepted. Absolute URLs (``https://...``) and + protocol relative URLs (``//host/path``) are ignored — otherwise the shop + could be used as an open redirect — as is any path under ``/shop/cart``, + which would loop back into a disabled route. In those cases ``/shop`` is + used and a warning is logged. + +Example: on a site running ``website_sale_aplicoop``, set it to ``/eskaera`` so +that every standard cart URL lands on the group order flow. + +Usage +===== + +There is nothing to do at runtime: once installed, the cart is disabled site +wide. To go back to the standard behaviour, uninstall the module — all template +modifications are removed with it and the ``website_sale`` cart works again as +usual. + +Files involved +============== + +- ``controllers/website_sale.py`` — overrides the ``website_sale`` cart routes. +- ``views/website_sale_disable_cart_templates.xml`` — templates that hide the + cart UI. +- ``models/res_config_settings.py`` and ``views/res_config_settings_views.xml`` + — the redirect URL setting. + +Installation +============ + +:: + + docker-compose run --rm odoo odoo -d odoo -i website_sale_disable_cart --stop-after-init + docker-compose up -d + +Testing +======= + +:: + + docker-compose run --rm odoo odoo -d odoo --test-enable --stop-after-init -u website_sale_disable_cart + +Credits +======= + +**Authors:** + +* Criptomart + +**History:** + +This behaviour was originally implemented inside ``website_sale_aplicoop`` and +was extracted into this module so that it can be installed and configured +independently of the group order flow. + +License +======= + +AGPL-3. See ``LICENSE.txt`` or +https://www.gnu.org/licenses/agpl-3.0-standalone.html diff --git a/website_sale_disable_cart/__init__.py b/website_sale_disable_cart/__init__.py new file mode 100644 index 0000000..91c5580 --- /dev/null +++ b/website_sale_disable_cart/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/website_sale_disable_cart/__manifest__.py b/website_sale_disable_cart/__manifest__.py new file mode 100644 index 0000000..3140b2a --- /dev/null +++ b/website_sale_disable_cart/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2026 - Today Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +{ # noqa: B018 + "name": "Website Sale - Disable Cart", + "version": "18.0.1.0.0", + "category": "Website/Sale", + "summary": "Turn the eCommerce shop into a read-only catalog: hide the cart UI " + "and redirect the standard cart routes", + "author": "Criptomart", + "maintainers": ["Criptomart"], + "website": "https://git.criptomart.net/criptomart/addons-cm", + "license": "AGPL-3", + "depends": [ + "website_sale", + ], + "data": [ + "views/res_config_settings_views.xml", + "views/website_sale_disable_cart_templates.xml", + ], + "installable": True, +} diff --git a/website_sale_disable_cart/controllers/__init__.py b/website_sale_disable_cart/controllers/__init__.py new file mode 100644 index 0000000..bdae4cc --- /dev/null +++ b/website_sale_disable_cart/controllers/__init__.py @@ -0,0 +1 @@ +from . import website_sale diff --git a/website_sale_disable_cart/controllers/website_sale.py b/website_sale_disable_cart/controllers/website_sale.py new file mode 100644 index 0000000..beb95ad --- /dev/null +++ b/website_sale_disable_cart/controllers/website_sale.py @@ -0,0 +1,102 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +import logging + +from odoo import http +from odoo.http import request + +from odoo.addons.website_sale.controllers.main import WebsiteSale + +_logger = logging.getLogger(__name__) + +DEFAULT_REDIRECT_URL = "/shop" +REDIRECT_URL_PARAM = "website_sale_disable_cart.redirect_url" + + +class WebsiteSaleDisableCart(WebsiteSale): + """Neutralize the standard ``website_sale`` cart endpoints. + + The shop keeps working as a plain catalog (listing, search and product + pages are untouched); only the cart itself becomes unreachable. HTTP + routes redirect to the configured URL, JSON routes answer with an inert + payload so any leftover frontend call is a no-op instead of an error. + """ + + def _get_cart_redirect_url(self): + """Return the internal path the disabled cart routes redirect to.""" + url = ( + request.env["ir.config_parameter"] + .sudo() + .get_param(REDIRECT_URL_PARAM, DEFAULT_REDIRECT_URL) + ) + url = (url or "").strip() + # Only site-internal paths are accepted: an absolute or protocol + # relative URL would turn the shop into an open redirect, and a path + # back under /shop/cart would loop through the disabled routes. + if ( + not url.startswith("/") + or url.startswith("//") + or url.startswith("/shop/cart") + ): + _logger.warning( + "[DISABLE_CART] Invalid redirect URL %r, falling back to %s", + url, + DEFAULT_REDIRECT_URL, + ) + return DEFAULT_REDIRECT_URL + return url + + def _redirect_disabled_cart(self, route): + """Redirect a disabled cart route to the configured URL.""" + url = self._get_cart_redirect_url() + _logger.info("[DISABLE_CART] %s → %s", route, url) + return request.redirect(url) + + @http.route() + def cart(self, access_token=None, revive="", **post): + """Cart page is disabled: send the visitor to the configured URL.""" + return self._redirect_disabled_cart("/shop/cart") + + @http.route() + def cart_update( + self, + product_id=None, + add_qty=1, + set_qty=0, + product_custom_attribute_values=None, + no_variant_attribute_value_ids=None, + **kwargs, + ): + """Adding to cart is disabled: nothing is written, just redirect.""" + return self._redirect_disabled_cart("/shop/cart/update") + + @http.route() + def cart_update_json( + self, + product_id=None, + line_id=None, + add_qty=None, + set_qty=None, + display=True, + product_custom_attribute_values=None, + no_variant_attribute_value_ids=None, + **kwargs, + ): + """Adding to cart is disabled. + + An empty dict is the response ``website_sale`` already returns when the + order cannot be updated, so callers handle it without breaking. + """ + _logger.info("[DISABLE_CART] /shop/cart/update_json ignored") + return {} + + @http.route() + def cart_quantity(self): + """The cart is always empty while this module is installed.""" + return 0 + + @http.route() + def clear_cart(self): + """Nothing to clear: the cart is never fed through the standard shop.""" + return None diff --git a/website_sale_disable_cart/i18n/es.po b/website_sale_disable_cart/i18n/es.po new file mode 100644 index 0000000..1981c45 --- /dev/null +++ b/website_sale_disable_cart/i18n/es.po @@ -0,0 +1,52 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_disable_cart +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_sale_disable_cart +#: model_terms:ir.ui.view,arch_db:website_sale_disable_cart.res_config_settings_view_form +msgid "Cart Redirect URL" +msgstr "URL de redirección del carrito" + +#. module: website_sale_disable_cart +#: model:ir.model,name:website_sale_disable_cart.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de configuración" + +#. module: website_sale_disable_cart +#: model_terms:ir.ui.view,arch_db:website_sale_disable_cart.res_config_settings_view_form +msgid "Disabled Cart" +msgstr "Carrito desactivado" + +#. module: website_sale_disable_cart +#: model:ir.model.fields,field_description:website_sale_disable_cart.field_res_config_settings__disabled_cart_redirect_url +msgid "Disabled Cart Redirect URL" +msgstr "URL de redirección del carrito desactivado" + +#. module: website_sale_disable_cart +#: model:ir.model.fields,help:website_sale_disable_cart.field_res_config_settings__disabled_cart_redirect_url +msgid "" +"Internal path visitors are sent to when they reach a standard cart URL. Must " +"start with '/' and must not point back to /shop/cart; otherwise '/shop' is " +"used." +msgstr "" +"Ruta interna a la que se envía a las personas visitantes cuando llegan a una " +"URL estándar del carrito. Debe empezar por '/' y no puede apuntar de nuevo a " +"/shop/cart; en caso contrario se usa '/shop'." + +#. module: website_sale_disable_cart +#: model_terms:ir.ui.view,arch_db:website_sale_disable_cart.res_config_settings_view_form +msgid "Internal path visitors land on when they reach a standard cart URL" +msgstr "" +"Ruta interna a la que se llega al entrar en una URL estándar del carrito" diff --git a/website_sale_disable_cart/i18n/eu.po b/website_sale_disable_cart/i18n/eu.po new file mode 100644 index 0000000..cf82761 --- /dev/null +++ b/website_sale_disable_cart/i18n/eu.po @@ -0,0 +1,52 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_disable_cart +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_sale_disable_cart +#: model_terms:ir.ui.view,arch_db:website_sale_disable_cart.res_config_settings_view_form +msgid "Cart Redirect URL" +msgstr "Saskiaren birbideratze URLa" + +#. module: website_sale_disable_cart +#: model:ir.model,name:website_sale_disable_cart.model_res_config_settings +msgid "Config Settings" +msgstr "Konfigurazio ezarpenak" + +#. module: website_sale_disable_cart +#: model_terms:ir.ui.view,arch_db:website_sale_disable_cart.res_config_settings_view_form +msgid "Disabled Cart" +msgstr "Saskia desgaituta" + +#. module: website_sale_disable_cart +#: model:ir.model.fields,field_description:website_sale_disable_cart.field_res_config_settings__disabled_cart_redirect_url +msgid "Disabled Cart Redirect URL" +msgstr "Desgaitutako saskiaren birbideratze URLa" + +#. module: website_sale_disable_cart +#: model:ir.model.fields,help:website_sale_disable_cart.field_res_config_settings__disabled_cart_redirect_url +msgid "" +"Internal path visitors are sent to when they reach a standard cart URL. Must " +"start with '/' and must not point back to /shop/cart; otherwise '/shop' is " +"used." +msgstr "" +"Bisitariak saskiaren URL estandar batera iristean bidaltzen zaien barne " +"bidea. '/' karakterearekin hasi behar da eta ezin du /shop/cart-era itzuli; " +"bestela '/shop' erabiltzen da." + +#. module: website_sale_disable_cart +#: model_terms:ir.ui.view,arch_db:website_sale_disable_cart.res_config_settings_view_form +msgid "Internal path visitors land on when they reach a standard cart URL" +msgstr "" +"Saskiaren URL estandar batera iristean bisitariak iristen diren barne bidea" diff --git a/website_sale_disable_cart/i18n/website_sale_disable_cart.pot b/website_sale_disable_cart/i18n/website_sale_disable_cart.pot new file mode 100644 index 0000000..3b1e055 --- /dev/null +++ b/website_sale_disable_cart/i18n/website_sale_disable_cart.pot @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_disable_cart +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_sale_disable_cart +#: model_terms:ir.ui.view,arch_db:website_sale_disable_cart.res_config_settings_view_form +msgid "Cart Redirect URL" +msgstr "" + +#. module: website_sale_disable_cart +#: model:ir.model,name:website_sale_disable_cart.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: website_sale_disable_cart +#: model_terms:ir.ui.view,arch_db:website_sale_disable_cart.res_config_settings_view_form +msgid "Disabled Cart" +msgstr "" + +#. module: website_sale_disable_cart +#: model:ir.model.fields,field_description:website_sale_disable_cart.field_res_config_settings__disabled_cart_redirect_url +msgid "Disabled Cart Redirect URL" +msgstr "" + +#. module: website_sale_disable_cart +#: model:ir.model.fields,help:website_sale_disable_cart.field_res_config_settings__disabled_cart_redirect_url +msgid "" +"Internal path visitors are sent to when they reach a standard cart URL. Must " +"start with '/' and must not point back to /shop/cart; otherwise '/shop' is " +"used." +msgstr "" + +#. module: website_sale_disable_cart +#: model_terms:ir.ui.view,arch_db:website_sale_disable_cart.res_config_settings_view_form +msgid "Internal path visitors land on when they reach a standard cart URL" +msgstr "" diff --git a/website_sale_disable_cart/models/__init__.py b/website_sale_disable_cart/models/__init__.py new file mode 100644 index 0000000..10fc65d --- /dev/null +++ b/website_sale_disable_cart/models/__init__.py @@ -0,0 +1 @@ +from . import res_config_settings # noqa: F401 diff --git a/website_sale_disable_cart/models/res_config_settings.py b/website_sale_disable_cart/models/res_config_settings.py new file mode 100644 index 0000000..7baaf94 --- /dev/null +++ b/website_sale_disable_cart/models/res_config_settings.py @@ -0,0 +1,18 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from odoo import fields +from odoo import models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + disabled_cart_redirect_url = fields.Char( + string="Disabled Cart Redirect URL", + config_parameter="website_sale_disable_cart.redirect_url", + default="/shop", + help="Internal path visitors are sent to when they reach a standard " + "cart URL. Must start with '/' and must not point back to /shop/cart; " + "otherwise '/shop' is used.", + ) diff --git a/website_sale_disable_cart/readme/CONFIGURE.rst b/website_sale_disable_cart/readme/CONFIGURE.rst new file mode 100644 index 0000000..56d8e04 --- /dev/null +++ b/website_sale_disable_cart/readme/CONFIGURE.rst @@ -0,0 +1,15 @@ +Go to *Website > Configuration > Settings > Disabled Cart* and set +**Cart Redirect URL**: the internal path visitors land on when they reach a +standard cart URL. + +* Default: ``/shop``. +* It is stored in the system parameter + ``website_sale_disable_cart.redirect_url``. +* Only site-internal paths are accepted. Absolute URLs (``https://...``) and + protocol relative URLs (``//host/path``) are ignored — otherwise the shop + could be used as an open redirect — as is any path under ``/shop/cart``, + which would loop back into a disabled route. In those cases ``/shop`` is + used and a warning is logged. + +Example: on a site running ``website_sale_aplicoop``, set it to ``/eskaera`` so +that every standard cart URL lands on the group order flow. diff --git a/website_sale_disable_cart/readme/CONTRIBUTORS.rst b/website_sale_disable_cart/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..07a462b --- /dev/null +++ b/website_sale_disable_cart/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* `Criptomart `_: + + * Development and maintenance + +Extracted from ``website_sale_aplicoop``, where this behaviour was originally +implemented. diff --git a/website_sale_disable_cart/readme/CREDITS.rst b/website_sale_disable_cart/readme/CREDITS.rst new file mode 100644 index 0000000..6a819ba --- /dev/null +++ b/website_sale_disable_cart/readme/CREDITS.rst @@ -0,0 +1,9 @@ +**Authors:** + +* Criptomart + +**Other credits:** + +This module extracts the "shop without cart" behaviour that was originally +implemented inside ``website_sale_aplicoop``, so that it can be installed (and +configured) independently of the group order flow. diff --git a/website_sale_disable_cart/readme/DESCRIPTION.rst b/website_sale_disable_cart/readme/DESCRIPTION.rst new file mode 100644 index 0000000..ace8a8a --- /dev/null +++ b/website_sale_disable_cart/readme/DESCRIPTION.rst @@ -0,0 +1,25 @@ +This module turns the standard Odoo eCommerce shop into a **read-only product +catalog**: visitors can browse, search and open product pages, but the standard +``website_sale`` shopping cart is not reachable any more. + +It is meant for sites where the actual ordering happens somewhere else — for +example the group order flow of ``website_sale_aplicoop`` (``/eskaera``), a +quotation request form, or an offline/phone process — while the public shop is +still useful as a catalog. + +What it does: + +* Hides the cart link and badge from all 12 standard website header styles + (default, mobile, hamburger, stretch, vertical, search, boxed, sidebar and + sales one to four). +* Removes the "Add to cart" quick-add area from the product listing cards. +* Neutralizes the standard cart endpoints: + + * ``/shop/cart`` and ``/shop/cart/update`` redirect to a configurable URL. + * ``/shop/cart/update_json`` returns an empty payload (no cart is written). + * ``/shop/cart/quantity`` always returns ``0``. + * ``/shop/cart/clear`` does nothing. + +The redirect target is configurable, so the module stays generic: point it to +``/shop`` (default) to keep visitors in the catalog, or to whatever route +implements the real ordering flow of your site. diff --git a/website_sale_disable_cart/readme/INSTALL.rst b/website_sale_disable_cart/readme/INSTALL.rst new file mode 100644 index 0000000..ec6acaf --- /dev/null +++ b/website_sale_disable_cart/readme/INSTALL.rst @@ -0,0 +1,13 @@ +Install it like any other addon:: + + docker-compose run --rm odoo odoo -d odoo -i website_sale_disable_cart --stop-after-init + docker-compose up -d + +**Upgrading from website_sale_aplicoop <= 18.0.1.11.0**: that module used to +carry these templates itself, under the same XPaths. Update it first so its old +views are dropped, and only then install this module — doing both in a single +Odoo run fails, because obsolete records are only cleaned up at the very end of +the run and both sets of templates would collide:: + + docker-compose run --rm odoo odoo -d odoo -u website_sale_aplicoop --stop-after-init + docker-compose run --rm odoo odoo -d odoo -i website_sale_disable_cart --stop-after-init diff --git a/website_sale_disable_cart/readme/USAGE.rst b/website_sale_disable_cart/readme/USAGE.rst new file mode 100644 index 0000000..697f7a1 --- /dev/null +++ b/website_sale_disable_cart/readme/USAGE.rst @@ -0,0 +1,10 @@ +There is nothing to do at runtime: once installed, the cart is disabled site +wide. + +To go back to the standard behaviour, simply uninstall the module. All the +template modifications are removed with it and the ``website_sale`` cart works +again as usual. + +Note that this module only disables the **cart**. Checkout and payment routes +are left untouched; with an empty cart they redirect back to the cart route, +and therefore to the configured URL. diff --git a/website_sale_disable_cart/tests/__init__.py b/website_sale_disable_cart/tests/__init__.py new file mode 100644 index 0000000..e4b3ea1 --- /dev/null +++ b/website_sale_disable_cart/tests/__init__.py @@ -0,0 +1 @@ +from . import test_disable_cart diff --git a/website_sale_disable_cart/tests/test_disable_cart.py b/website_sale_disable_cart/tests/test_disable_cart.py new file mode 100644 index 0000000..95d9b63 --- /dev/null +++ b/website_sale_disable_cart/tests/test_disable_cart.py @@ -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) diff --git a/website_sale_disable_cart/views/res_config_settings_views.xml b/website_sale_disable_cart/views/res_config_settings_views.xml new file mode 100644 index 0000000..f28e5b7 --- /dev/null +++ b/website_sale_disable_cart/views/res_config_settings_views.xml @@ -0,0 +1,29 @@ + + + + res.config.settings.view.form.inherit.disable.cart + res.config.settings + + + +

Disabled Cart

+
+
+
+
+
+
+
+ + + + diff --git a/website_sale_aplicoop/views/website_sale_disable_cart.xml b/website_sale_disable_cart/views/website_sale_disable_cart_templates.xml similarity index 71% rename from website_sale_aplicoop/views/website_sale_disable_cart.xml rename to website_sale_disable_cart/views/website_sale_disable_cart_templates.xml index 842ec3f..89173dc 100644 --- a/website_sale_aplicoop/views/website_sale_disable_cart.xml +++ b/website_sale_disable_cart/views/website_sale_disable_cart_templates.xml @@ -3,7 +3,7 @@