[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
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
|
@ -182,6 +182,7 @@ addons-cm/
|
||||||
├── account_invoice_triple_discount_readonly/ # Fix bug acumulación triple descuento
|
├── account_invoice_triple_discount_readonly/ # Fix bug acumulación triple descuento
|
||||||
├── # --- Ventas y web ---
|
├── # --- Ventas y web ---
|
||||||
├── website_sale_aplicoop/ # Sistema eskaera (compras grupo)
|
├── 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
|
├── portal_event_registration/ # Portal: ver registros de eventos + adjuntos
|
||||||
├── # --- Membresías ---
|
├── # --- Membresías ---
|
||||||
├── membership_monthly_invoicing/ # Factura mensual de membresía por socio (cron)
|
├── membership_monthly_invoicing/ # Factura mensual de membresía por socio (cron)
|
||||||
|
|
@ -220,6 +221,7 @@ addons-cm/
|
||||||
**Ventas y web**
|
**Ventas y web**
|
||||||
|
|
||||||
- [website_sale_aplicoop](../website_sale_aplicoop/README.rst) - Sistema eskaera completo
|
- [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
|
- [portal_event_registration](../portal_event_registration/README.rst) - Portal: registros de eventos + adjuntos al chatter
|
||||||
|
|
||||||
**Membresías**
|
**Membresías**
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ Sales & website:
|
||||||
|
|
||||||
- `website_sale_aplicoop` — Eskaera: collaborative purchasing for consumer co-ops (group orders,
|
- `website_sale_aplicoop` — Eskaera: collaborative purchasing for consumer co-ops (group orders,
|
||||||
per-member carts, cutoff/pickup dates, lazy loading, multi-language).
|
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.
|
- `portal_event_registration` — portal users view their event registrations + upload attachments to chatter.
|
||||||
|
|
||||||
Membership:
|
Membership:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,24 @@
|
||||||
# Changelog - Website Sale Aplicoop
|
# 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
|
## [18.0.1.11.0] - 2026-07-15
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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
|
This module adds the "eskaera" group order flow (``/eskaera``); it no longer
|
||||||
``/shop`` storefront into a simple product catalog (no standard ``website_sale``
|
touches the standard ``website_sale`` storefront. The default ``/shop`` and its
|
||||||
shopping cart). The change is intentional for sites that use the Aplicoop
|
shopping cart keep working as usual after installing it.
|
||||||
"eskaera" flow as the single shopping experience.
|
|
||||||
|
|
||||||
What the module does
|
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
|
||||||
- Hides the standard header cart link and badge.
|
standard cart endpoints to a configurable URL. That behaviour used to live in
|
||||||
- Removes the "Add to cart" quick-add area from product listings.
|
this module (up to 18.0.1.11.0) and was extracted in 18.0.1.12.0.
|
||||||
- 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):
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
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
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
To revert back to the standard ``website_sale`` behaviour:
|
Then set *Website > Configuration > Settings > Disabled Cart > Cart Redirect
|
||||||
|
URL* to ``/eskaera``.
|
||||||
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.
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ Sistema de pedidos colaborativos para grupos de consumo. Reemplaza el legacy Apl
|
||||||
## Resumen de funcionalidades
|
## Resumen de funcionalidades
|
||||||
|
|
||||||
- Gestión completa de órdenes de grupo (draft → confirmed → collected → invoiced → completed)
|
- 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)
|
- 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
|
- Cron diario de auto-confirmación + creación automática de lotes de picking
|
||||||
- Paginación de productos (lazy loading) para cargas rápidas
|
- Paginación de productos (lazy loading) para cargas rápidas
|
||||||
|
|
@ -86,7 +87,9 @@ Todos los endpoints viven bajo `/eskaera/`:
|
||||||
| `POST /eskaera/<order_id>/confirm` | Confirmar carrito (sale.order en draft) |
|
| `POST /eskaera/<order_id>/confirm` | Confirmar carrito (sale.order en draft) |
|
||||||
| `POST /eskaera/clear-cart` | Limpiar carrito actual |
|
| `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`.
|
**Seguridad portal:** los usuarios solo ven órdenes de su `consumer_group_id`. La regla `rule_group_order_company_read` incluye guardia `user.share`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
{ # noqa: B018
|
{ # noqa: B018
|
||||||
"name": "Website Sale - Aplicoop",
|
"name": "Website Sale - Aplicoop",
|
||||||
"version": "18.0.1.11.0",
|
"version": "18.0.1.12.0",
|
||||||
"category": "Website/Sale",
|
"category": "Website/Sale",
|
||||||
"summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders",
|
"summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders",
|
||||||
"author": "Odoo Community Association (OCA), Criptomart",
|
"author": "Odoo Community Association (OCA), Criptomart",
|
||||||
|
|
@ -39,7 +39,6 @@
|
||||||
"views/res_partner_views.xml",
|
"views/res_partner_views.xml",
|
||||||
"views/res_config_settings_views.xml",
|
"views/res_config_settings_views.xml",
|
||||||
"views/website_templates.xml",
|
"views/website_templates.xml",
|
||||||
"views/website_sale_disable_cart.xml",
|
|
||||||
"views/product_template_views.xml",
|
"views/product_template_views.xml",
|
||||||
"views/sale_order_views.xml",
|
"views/sale_order_views.xml",
|
||||||
"views/stock_picking_views.xml",
|
"views/stock_picking_views.xml",
|
||||||
|
|
|
||||||
|
|
@ -2457,50 +2457,3 @@ class AplicoopWebsiteSale(WebsiteSale):
|
||||||
"empty_cart": "Your cart is empty",
|
"empty_cart": "Your cart is empty",
|
||||||
"added_to_cart": "added to cart",
|
"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")
|
|
||||||
|
|
|
||||||
56
website_sale_disable_cart/LICENSE.txt
Normal file
56
website_sale_disable_cart/LICENSE.txt
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
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.
|
||||||
106
website_sale_disable_cart/README.rst
Normal file
106
website_sale_disable_cart/README.rst
Normal file
|
|
@ -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
|
||||||
2
website_sale_disable_cart/__init__.py
Normal file
2
website_sale_disable_cart/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
22
website_sale_disable_cart/__manifest__.py
Normal file
22
website_sale_disable_cart/__manifest__.py
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
1
website_sale_disable_cart/controllers/__init__.py
Normal file
1
website_sale_disable_cart/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import website_sale
|
||||||
102
website_sale_disable_cart/controllers/website_sale.py
Normal file
102
website_sale_disable_cart/controllers/website_sale.py
Normal file
|
|
@ -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
|
||||||
52
website_sale_disable_cart/i18n/es.po
Normal file
52
website_sale_disable_cart/i18n/es.po
Normal file
|
|
@ -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"
|
||||||
52
website_sale_disable_cart/i18n/eu.po
Normal file
52
website_sale_disable_cart/i18n/eu.po
Normal file
|
|
@ -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"
|
||||||
47
website_sale_disable_cart/i18n/website_sale_disable_cart.pot
Normal file
47
website_sale_disable_cart/i18n/website_sale_disable_cart.pot
Normal file
|
|
@ -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 ""
|
||||||
1
website_sale_disable_cart/models/__init__.py
Normal file
1
website_sale_disable_cart/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import res_config_settings # noqa: F401
|
||||||
18
website_sale_disable_cart/models/res_config_settings.py
Normal file
18
website_sale_disable_cart/models/res_config_settings.py
Normal file
|
|
@ -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.",
|
||||||
|
)
|
||||||
15
website_sale_disable_cart/readme/CONFIGURE.rst
Normal file
15
website_sale_disable_cart/readme/CONFIGURE.rst
Normal file
|
|
@ -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.
|
||||||
6
website_sale_disable_cart/readme/CONTRIBUTORS.rst
Normal file
6
website_sale_disable_cart/readme/CONTRIBUTORS.rst
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
* `Criptomart <https://criptomart.net>`_:
|
||||||
|
|
||||||
|
* Development and maintenance
|
||||||
|
|
||||||
|
Extracted from ``website_sale_aplicoop``, where this behaviour was originally
|
||||||
|
implemented.
|
||||||
9
website_sale_disable_cart/readme/CREDITS.rst
Normal file
9
website_sale_disable_cart/readme/CREDITS.rst
Normal file
|
|
@ -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.
|
||||||
25
website_sale_disable_cart/readme/DESCRIPTION.rst
Normal file
25
website_sale_disable_cart/readme/DESCRIPTION.rst
Normal file
|
|
@ -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.
|
||||||
13
website_sale_disable_cart/readme/INSTALL.rst
Normal file
13
website_sale_disable_cart/readme/INSTALL.rst
Normal file
|
|
@ -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
|
||||||
10
website_sale_disable_cart/readme/USAGE.rst
Normal file
10
website_sale_disable_cart/readme/USAGE.rst
Normal file
|
|
@ -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.
|
||||||
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)
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.inherit.disable.cart</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//block[@id='website_info_settings']" position="after">
|
||||||
|
<h2>Disabled Cart</h2>
|
||||||
|
<div class="row mt16 o_settings_container" id="disable_cart_settings">
|
||||||
|
<div class="col-12 col-lg-6 o_setting_box">
|
||||||
|
<div class="o_setting_left_pane"/>
|
||||||
|
<div class="o_setting_right_pane">
|
||||||
|
<label for="disabled_cart_redirect_url" string="Cart Redirect URL"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
Internal path visitors land on when they reach a standard cart URL
|
||||||
|
</div>
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="mt16">
|
||||||
|
<field name="disabled_cart_redirect_url" class="oe_inline" placeholder="/shop"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<data>
|
<data>
|
||||||
<!-- ==========================================
|
<!-- ==========================================
|
||||||
DISABLE STANDARD WEBSITE_SALE CART
|
DISABLE STANDARD WEBSITE_SALE CART
|
||||||
Convert /shop to a simple product catalog
|
Convert /shop into a simple product catalog
|
||||||
========================================== -->
|
========================================== -->
|
||||||
|
|
||||||
<!-- Hide the cart link from the header by removing the t-call to website_sale.header_cart_link
|
<!-- Hide the cart link from the header by removing the t-call to website_sale.header_cart_link
|
||||||
|
|
@ -36,6 +36,26 @@
|
||||||
<xpath expr="//t[@t-call='website_sale.header_cart_link']" position="replace"/>
|
<xpath expr="//t[@t-call='website_sale.header_cart_link']" position="replace"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="hide_cart_in_header_sales_two" inherit_id="website.template_header_sales_two" name="Hide Cart Link in Header Sales Two">
|
||||||
|
<xpath expr="//t[@t-call='website_sale.header_cart_link']" position="replace"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="hide_cart_in_header_sales_three" inherit_id="website.template_header_sales_three" name="Hide Cart Link in Header Sales Three">
|
||||||
|
<xpath expr="//t[@t-call='website_sale.header_cart_link']" position="replace"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="hide_cart_in_header_sales_four" inherit_id="website.template_header_sales_four" name="Hide Cart Link in Header Sales Four">
|
||||||
|
<xpath expr="//t[@t-call='website_sale.header_cart_link']" position="replace"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="hide_cart_in_header_boxed" inherit_id="website.template_header_boxed" name="Hide Cart Link in Header Boxed">
|
||||||
|
<xpath expr="//t[@t-call='website_sale.header_cart_link']" position="replace"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="hide_cart_in_header_sidebar" inherit_id="website.template_header_sidebar" name="Hide Cart Link in Header Sidebar">
|
||||||
|
<xpath expr="//t[@t-call='website_sale.header_cart_link']" position="replace"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Remove "Add to Cart" button from product items in the shop -->
|
<!-- Remove "Add to Cart" button from product items in the shop -->
|
||||||
<template id="products_item_no_add_to_cart" inherit_id="website_sale.products_item" name="Product Item Without Add to Cart">
|
<template id="products_item_no_add_to_cart" inherit_id="website_sale.products_item" name="Product Item Without Add to Cart">
|
||||||
<!-- Remove the form action that points to /shop/cart/update -->
|
<!-- Remove the form action that points to /shop/cart/update -->
|
||||||
Loading…
Add table
Add a link
Reference in a new issue