addons-cm/website_sale_aplicoop/README_DEV.md
GitHub Copilot ef1283be7c [REF] stock_picking_batch_collect: rename and drop the aplicoop dependency
"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>
2026-08-27 14:26:47 +02:00

213 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (06) | 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)