Compare commits

..

No commits in common. "f2194c5367b5d6a47d6d778b4f3a7c5dc83f4c63" and "f14aab003a6924a61f58d8c7d9827be30063781b" have entirely different histories.

31 changed files with 177 additions and 733 deletions

View file

@ -182,7 +182,6 @@ 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)
@ -221,7 +220,6 @@ 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**

View file

@ -58,8 +58,6 @@ 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:

View file

@ -1 +1,3 @@
from . import account_move # noqa: F401 from . import account_move # noqa: F401
from . import res_company # noqa: F401
from . import l10n_es_edi_tbai_document # noqa: F401

View file

@ -0,0 +1,62 @@
# Copyright 2026 - Today Criptomart
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import re
from odoo import models
class L10nEsEdiTbaiDocument(models.Model):
_inherit = "l10n_es_edi_tbai.document"
def _get_sale_values(self, values):
"""Override to ensure chain links only within the same invoice series."""
sale_values = super()._get_sale_values(values)
# Replace chain_prev_document with one from the same series
chain_prev_document = sale_values.get("chain_prev_document")
if chain_prev_document:
current_series = self._get_series_prefix(self.name)
prev_series = self._get_series_prefix(chain_prev_document.name)
# If series mismatch OR previous doc has no valid XML, find correct one
if (
current_series != prev_series
or not chain_prev_document.xml_attachment_id
):
same_series_doc = self._get_last_chained_document_by_series(
current_series
)
sale_values["chain_prev_document"] = same_series_doc
return sale_values
@staticmethod
def _get_series_prefix(doc_name):
"""Extract series prefix from document name (e.g., 'INV/2026/' from 'INV/2026/01248')."""
if not doc_name:
return ""
# Everything before the first digit
match = re.search(r"\d+", doc_name)
if match:
return doc_name[: match.start()].rstrip("/")
return doc_name
def _get_last_chained_document_by_series(self, series_prefix):
"""Find the last valid chained document matching a specific series prefix."""
domain = [
("chain_index", "!=", 0),
("company_id", "=", self.company_id.id),
("is_cancel", "=", False),
("state", "=", "accepted"), # Must be accepted in chain
]
docs = self.search(domain, order="chain_index desc")
# Return the first doc matching series that has valid XML
for doc in docs:
if (
self._get_series_prefix(doc.name) == series_prefix
and doc.xml_attachment_id
):
return doc
return self.env["l10n_es_edi_tbai.document"]

View file

@ -0,0 +1,22 @@
# Copyright 2026 - Today Criptomart
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
class ResCompany(models.Model):
_inherit = "res.company"
def _get_l10n_es_tbai_last_chained_document(self):
"""
Returns the last tbai document posted to this company's chain.
Each invoice series has its own independent chain in TicketBAI.
Filters out cancellation documents to skip cancelled invoices.
"""
domain = [
("chain_index", "!=", 0),
("company_id", "=", self.id),
("is_cancel", "=", False),
]
return self.env["l10n_es_edi_tbai.document"].search(
domain, limit=1, order="chain_index desc"
)

View file

@ -1,24 +1,5 @@
# 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

View file

@ -142,22 +142,48 @@ 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 - Standard shop and cart Notes - Shop behaves as a simple catalog
============================== =========================================
This module adds the "eskaera" group order flow (``/eskaera``); it no longer Starting with the recent update, this module converts the default Odoo
touches the standard ``website_sale`` storefront. The default ``/shop`` and its ``/shop`` storefront into a simple product catalog (no standard ``website_sale``
shopping cart keep working as usual after installing it. shopping cart). The change is intentional for sites that use the Aplicoop
"eskaera" flow as the single shopping experience.
Sites that want the standard cart disabled — so that the group order flow is What the module does
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 - Hides the standard header cart link and badge.
this module (up to 18.0.1.11.0) and was extracted in 18.0.1.12.0. - 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):
:: ::
docker-compose run --rm odoo odoo -d odoo -i website_sale_disable_cart --stop-after-init docker-compose run --rm odoo odoo -d odoo -u website_sale_aplicoop --stop-after-init
docker-compose up -d
Then set *Website > Configuration > Settings > Disabled Cart > Cart Redirect To revert back to the standard ``website_sale`` behaviour:
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.

View file

@ -7,8 +7,7 @@ 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 (la tienda estándar de `website_sale` se - Carrito separado por grupo de consumo; tienda estándar de `website_sale` deshabilitada
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
@ -87,9 +86,7 @@ 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 estándar de `website_sale` (`/shop` y su carrito) no se tocan desde Las rutas `/cart` y `/shop` de `website_sale` están redirigidas a `/eskaera`.
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`.

View file

@ -3,7 +3,7 @@
{ # noqa: B018 { # noqa: B018
"name": "Website Sale - Aplicoop", "name": "Website Sale - Aplicoop",
"version": "18.0.1.12.0", "version": "18.0.1.11.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,6 +39,7 @@
"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",

View file

@ -2457,3 +2457,50 @@ 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")

View file

@ -3,7 +3,7 @@
<data> <data>
<!-- ========================================== <!-- ==========================================
DISABLE STANDARD WEBSITE_SALE CART DISABLE STANDARD WEBSITE_SALE CART
Convert /shop into a simple product catalog Convert /shop to 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,26 +36,6 @@
<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 -->

View file

@ -1,56 +0,0 @@
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.

View file

@ -1,106 +0,0 @@
===============================
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

View file

@ -1,2 +0,0 @@
from . import controllers
from . import models

View file

@ -1,22 +0,0 @@
# 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,
}

View file

@ -1 +0,0 @@
from . import website_sale

View file

@ -1,102 +0,0 @@
# 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

View file

@ -1,52 +0,0 @@
# 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"

View file

@ -1,52 +0,0 @@
# 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"

View file

@ -1,47 +0,0 @@
# 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 ""

View file

@ -1 +0,0 @@
from . import res_config_settings # noqa: F401

View file

@ -1,18 +0,0 @@
# 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.",
)

View file

@ -1,15 +0,0 @@
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.

View file

@ -1,6 +0,0 @@
* `Criptomart <https://criptomart.net>`_:
* Development and maintenance
Extracted from ``website_sale_aplicoop``, where this behaviour was originally
implemented.

View file

@ -1,9 +0,0 @@
**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.

View file

@ -1,25 +0,0 @@
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.

View file

@ -1,13 +0,0 @@
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

View file

@ -1,10 +0,0 @@
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.

View file

@ -1 +0,0 @@
from . import test_disable_cart

View file

@ -1,103 +0,0 @@
# 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)

View file

@ -1,29 +0,0 @@
<?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>