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>
199 lines
7.7 KiB
Markdown
199 lines
7.7 KiB
Markdown
# Website Sale - Aplicoop
|
||
|
||
**Versión:** 18.0.1.9.0 | **Licencia:** AGPL-3 | **Autor:** Criptomart SL
|
||
|
||
Sistema de pedidos colaborativos para grupos de consumo. Reemplaza el legacy Aplicoop con una solución moderna integrada en Odoo 18.
|
||
|
||
## Resumen de funcionalidades
|
||
|
||
- 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
|
||
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
|
||
- Soporte multilingüe: ES, EU, PT, GL, CA, FR, IT
|
||
- Restricción de acceso portal por grupo de consumo
|
||
|
||
## Modelos
|
||
|
||
### `group.order`
|
||
|
||
Orden de grupo central. Contiene toda la lógica de ciclo de vida.
|
||
|
||
**Campos principales:**
|
||
|
||
| Campo | Tipo | Descripción |
|
||
| ----- | ---- | ----------- |
|
||
| `name` | Char | Identificador de la orden |
|
||
| `consumer_group_id` | Many2one → `res.partner` | Grupo de consumo |
|
||
| `state` | Selection | draft / confirmed / collected / invoiced / completed / cancelled |
|
||
| `cutoff_date` | Datetime | Hasta cuándo se pueden añadir productos |
|
||
| `pickup_date` | Date | Día de recogida para los socios |
|
||
| `delivery_date` | Date | Fecha de entrega (almacenada, calculada por cron) |
|
||
| `product_ids` | Many2many | Productos incluidos directamente |
|
||
| `category_ids` | Many2many | Categorías incluidas (y sus subcategorías) |
|
||
| `supplier_ids` | Many2many | Proveedores incluidos (por `main_seller_id`) |
|
||
| `excluded_product_ids` | Many2many | Blacklist de productos |
|
||
| `excluded_supplier_ids` | Many2many | Blacklist de proveedores |
|
||
| `excluded_category_ids` | Many2many | Blacklist de categorías (recursiva) |
|
||
| `allow_home_delivery` | Boolean | Permite envío a domicilio además de recogida |
|
||
|
||
**Métodos clave:**
|
||
|
||
- `_get_products_for_group_order()` — catálogo efectivo aplicando whitelist + blacklist
|
||
- `_get_products_paginated(page, per_page)` — paginación para lazy loading
|
||
- `_cron_confirm_group_orders()` — auto-confirma órdenes pasado el cutoff y crea lotes de picking
|
||
- `_cron_update_dates()` — recalcula fechas de entrega/recogida diariamente
|
||
|
||
### `group.order.slot` *(en desarrollo)*
|
||
|
||
Franjas horarias de recogida para una orden de grupo.
|
||
|
||
| Campo | Tipo | Descripción |
|
||
| ----- | ---- | ----------- |
|
||
| `group_order_id` | Many2one | Orden de grupo |
|
||
| `weekday` | Selection (0–6) | Día de la semana (0 = lunes) |
|
||
| `start_hour` / `end_hour` | Float | Horario en formato decimal (9.5 = 09:30) |
|
||
| `sequence` | Integer | Orden de visualización |
|
||
|
||
### Extensiones de modelos core
|
||
|
||
**`product.template`**
|
||
|
||
- `main_seller_id` — proveedor principal (de `product_main_seller`)
|
||
- `sequence` en `product.category` — orden de visualización en la tienda web
|
||
|
||
**`sale.order`**
|
||
|
||
- `group_order_id` — enlace a la orden de grupo
|
||
- `consumer_group_id` — propagado desde `group_order_id`
|
||
- `pickup_slot_label` — etiqueta legible de la franja de recogida
|
||
|
||
**`stock.picking`**
|
||
|
||
- Extensión para agrupación por grupo de consumo en lotes de picking
|
||
|
||
## Controladores
|
||
|
||
Todos los endpoints viven bajo `/eskaera/`:
|
||
|
||
| Ruta | Descripción |
|
||
| ---- | ----------- |
|
||
| `GET /eskaera` | Lista de órdenes de grupo activas del usuario |
|
||
| `GET /eskaera/<order_id>` | Tienda de la orden (con lazy loading) |
|
||
| `GET /eskaera/<order_id>/load-page?page=N` | Carga AJAX de página de productos |
|
||
| `POST /eskaera/<order_id>/add` | Añadir producto al carrito |
|
||
| `POST /eskaera/<order_id>/confirm` | Confirmar carrito (sale.order en draft) |
|
||
| `POST /eskaera/clear-cart` | Limpiar carrito actual |
|
||
|
||
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`.
|
||
|
||
## Templates QWeb
|
||
|
||
| Template | Descripción |
|
||
| -------- | ----------- |
|
||
| `eskaera_list` | Lista de órdenes activas |
|
||
| `eskaera_shop` | Página de la tienda (incluye `eskaera_shop_products`) |
|
||
| `eskaera_shop_products` | Grid de productos (reutilizable por lazy loading + initial render) |
|
||
| `eskaera_cart` | Sidebar del carrito |
|
||
| `eskaera_checkout` | Confirmación del pedido |
|
||
| `load_from_history` | Cargar productos de una orden histórica |
|
||
|
||
## JavaScript (`website_sale.js`)
|
||
|
||
Clase `GroupOrderShop` (extiende `publicWidget.Widget`):
|
||
|
||
- `_attachEventListeners()` — inicializa todos los listeners
|
||
- `_attachLoadMoreListener()` — botón "Cargar más" para lazy loading (AJAX)
|
||
- `_onAddToCart()` — añadir al carrito con feedback visual
|
||
- `_onClearCart()` — limpiar carrito
|
||
- `_onDeliveryToggle()` — toggle envío a domicilio / recogida
|
||
|
||
## Control del catálogo
|
||
|
||
La lógica en `_get_products_for_group_order()` aplica en este orden:
|
||
|
||
1. **Whitelist**: unión de `product_ids` + productos de `category_ids` (recursivo) + productos de `supplier_ids` (por `main_seller_id`)
|
||
2. **Blacklist**: se restan `excluded_product_ids`, productos con `main_seller_id` en `excluded_supplier_ids`, y productos en `excluded_category_ids` (recursivo)
|
||
3. La blacklist tiene prioridad absoluta sobre cualquier fuente de inclusión
|
||
|
||
## Lazy loading
|
||
|
||
Configurable desde **Ajustes > Website > Shop**:
|
||
|
||
- `website_sale_aplicoop.lazy_loading_enabled` (default: `True`)
|
||
- `website_sale_aplicoop.products_per_page` (default: `20`)
|
||
|
||
La página inicial renderiza la primera página server-side. El botón "Cargar más" hace peticiones AJAX al endpoint `/load-page` que devuelve HTML parcial con el template `eskaera_shop_products`.
|
||
|
||
## Cron jobs
|
||
|
||
| Cron | Frecuencia | Acción |
|
||
| ---- | ---------- | ------ |
|
||
| `_cron_confirm_group_orders` | Diario (medianoche) | Confirma sale.orders pasado el cutoff; crea lotes de picking agrupados por consumer_group + pickup_date |
|
||
| `_cron_update_dates` | Diario | Recalcula `delivery_date` y `pickup_date` en órdenes activas |
|
||
|
||
## Dependencias
|
||
|
||
```text
|
||
website_sale_aplicoop
|
||
├── website_sale (Odoo core)
|
||
├── sale (Odoo core)
|
||
├── product_main_seller (OCA)
|
||
├── product_sale_price_from_pricelist (custom)
|
||
│ └── product_pricelist_total_margin (custom)
|
||
│ └── product_price_category (OCA)
|
||
└── stock_picking_batch_custom (custom)
|
||
```
|
||
|
||
## Instalación y actualización
|
||
|
||
```bash
|
||
docker-compose exec -T odoo odoo -d odoo -u website_sale_aplicoop --stop-after-init
|
||
```
|
||
|
||
Para migraciones con cambios de esquema (e.g., nuevas columnas):
|
||
|
||
```bash
|
||
docker-compose exec -T odoo odoo -d odoo --update website_sale_aplicoop --stop-after-init
|
||
```
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
docker-compose exec -T odoo odoo -d odoo --test-enable --stop-after-init -u website_sale_aplicoop
|
||
```
|
||
|
||
Los tests están en `tests/`:
|
||
|
||
- `test_group_order.py` — ciclo de vida básico, fechas, cron
|
||
- `test_product_discovery.py` — whitelist/blacklist (productos, proveedores, categorías)
|
||
- `test_confirm_eskaera.py` — integración: confirmación de carrito y generación de sale.order
|
||
|
||
## Traducciones
|
||
|
||
```bash
|
||
# Exportar .pot
|
||
docker-compose exec -T odoo odoo -d odoo \
|
||
-i website_sale_aplicoop \
|
||
--i18n-export=/tmp/website_sale_aplicoop.pot \
|
||
--stop-after-init
|
||
|
||
# Actualizar .po existentes
|
||
cd website_sale_aplicoop/i18n
|
||
for lang in es eu; do
|
||
msgmerge -U ${lang}.po ../website_sale_aplicoop.pot
|
||
done
|
||
```
|
||
|
||
Ver [docs/TRANSLATIONS.md](../docs/TRANSLATIONS.md) para convenciones.
|
||
|
||
## Repositorio
|
||
|
||
- Repo: <https://git.criptomart.net/criptomart/addons-cm>
|
||
- Changelog: [CHANGELOG.md](CHANGELOG.md)
|