# 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; tienda estándar de `website_sale` deshabilitada
- 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 `/cart` y `/shop` de `website_sale` están redirigidas a `/eskaera`.

**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)
