"custom" said nothing about what the module does. It is really about collecting goods into baskets: the extra detailed-operation columns, the is_collected flag, the Product Summary tab, the per-company validation restrictions and the Basket Assembly operator view all serve that one job. Rename it accordingly. Invert the dependency while at it. A generic warehouse addon was dragging in an entire eCommerce application, and the whole coupling was a single field: stock.move.line.home_delivery, related to picking_id.home_delivery. Everything else was already duck-typed. website_sale_aplicoop now depends on this module and injects its own consumer group columns into these views. This removes duplicated logic rather than relocating it: stock.picking .batch_consumer_group_id re-derived from sale_id a value aplicoop already stored as stock.picking.consumer_group_id, and the duplicate carried no @api.depends, so it never recomputed reliably. The batch transfers list now shows the stored field, which is sortable and groupable. The two aplicoop tests that probed information_schema for the res_company batch_* columns can drop that guard: a real dependency guarantees them. Renaming an addon is not something a migrations/ script can do, since a renamed addon is a brand new module to Odoo and its migration scripts never run. A pre_init_hook does it instead: it fires on install after the Python is imported but before registry.load(), which is the window where remapping ir_model_data makes Odoo reuse the existing tables and columns. is_collected, the summary line table and the company settings all survive untouched. Two details the hook has to get right: - ir_model_constraint.module and ir_model_relation.module are integer FKs with ON DELETE CASCADE, so they must be repointed before the old module row is deleted or the bookkeeping goes with it. - Deleting an ir_model_data row does not cascade to the record it points at. Artifacts handed over to aplicoop only need the xmlid dropped, but artifacts that disappear need the record deleted too, or the field survives as an orphan manual field and the view as a custom view referencing it. Verified against a restored copy of the dev database: 50 xmlids moved, 19 summary lines and 5 collected move lines preserved, no orphans, both test suites green, and the module installs cleanly on a database without website_sale_aplicoop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
213 lines
8.3 KiB
Markdown
213 lines
8.3 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 por pedido de grupo, uno por tipo de operación (`_create_picking_batches`) |
|
||
| `_cron_update_dates` | Diario | Recalcula `delivery_date` y `pickup_date` en órdenes activas |
|
||
|
||
## Dependencias
|
||
|
||
Según `__manifest__.py`:
|
||
|
||
```text
|
||
website_sale_aplicoop
|
||
├── website_sale (Odoo core)
|
||
├── website_sale_stock (Odoo core)
|
||
├── payment (Odoo core)
|
||
├── product (Odoo core)
|
||
├── sale (Odoo core)
|
||
├── stock (Odoo core)
|
||
├── account (Odoo core)
|
||
├── stock_picking_batch (Odoo core)
|
||
├── stock_picking_batch_collect (custom)
|
||
│ ├── stock_move_manual_quantity (custom)
|
||
│ └── stock_picking_batch (Odoo core)
|
||
├── product_get_price_helper (OCA)
|
||
└── l10n_es_partner (OCA)
|
||
```
|
||
|
||
La dependencia sobre `stock_picking_batch_collect` es deliberada y va en este
|
||
sentido: ese addon es genérico de almacén y no sabe nada de grupos de consumo.
|
||
Es `website_sale_aplicoop` quien inyecta en sus vistas las columnas
|
||
**Consumer Group** y **Home Delivery** (ver `views/stock_picking_batch_views.xml`
|
||
y `models/stock_move_line_extension.py`).
|
||
|
||
## 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)
|