# Reglas del Proyecto — Hazard (Odoo 16)

## Información General
- **Empresa:** Hazard (marca de ropa urbana / streetwear)
- **Plataforma ERP:** Odoo 16 Community con módulos Enterprise
- **Ecommerce:** Shopify (sincronizado con Odoo vía módulo custom `hazard_shopify`)
- **Servidor:** VPS en Plesk bajo el dominio `tesscorp.com.mx`
- **Idioma de comunicación:** Siempre responder en español

## Arquitectura Docker

### Contenedores
| Contenedor | Imagen | Puerto Externo | Puerto Interno |
|---|---|---|---|
| `odoo-16-hazard-new` | `hazard-odoo-odoo` (custom, basada en `odoo:16`) | `16001` | `8069` |
| `odoo-16-db-hazard-new` | `postgres:17` | No expuesto | `5432` |

### Volúmenes y Rutas
- **Módulos Enterprise:** `./Addons/enterprise` → `/mnt/enterprise` (dentro del contenedor)
- **Módulos Custom:** `./Addons/custom` → `/mnt/extra-addons` (dentro del contenedor)
- **Configuración Odoo:** `./Conf/odoo.conf` → `/etc/odoo/odoo.conf`
- **Docker Compose:** `/var/www/vhosts/tesscorp.com.mx/hazard.tesscorp.com.mx/docker-compose.yml`
- **IP interna del contenedor DB:** `172.18.0.2` (usada por MCP Server PostgreSQL)

### Base de Datos
- **Base de datos principal (producción):** `hazard_new`
- **Usuario DB:** `odoo`
- **Contraseña DB:** `odoo`

### Credenciales Odoo (XML-RPC)
- **URL:** `http://localhost:16001`
- **Base de datos:** `hazard_new`
- **Usuario admin:** `admin`
- **Contraseña admin:** `admin`

## Módulos Custom Instalados
| Módulo | Estado | Ubicación |
|---|---|---|
| `hazard_shopify` | ✅ Instalado | `Addons/custom/hazard_shopify` |
| `hazard_inventory_report` | ✅ Instalado | `Addons/custom/hazard_inventory_report` |
| `hazard_website` | ❌ No instalado | `Addons/custom/hazard_website` |

### Otros Módulos de Terceros
- `bi_sql_editor` — Editor SQL en interfaz Odoo
- `query_deluxe` — Consultas avanzadas desde Odoo
- `sql_request_abstract` — Abstracción para consultas SQL
- `l10n_mx_sat_sync_itadmin_ee` — Sincronización SAT México

## Reglas de Desarrollo Obligatorias

### Python
- **NUNCA** usar `print()`. Siempre usar `logging` con `_logger = logging.getLogger(__name__)`.
- Usar prefijos descriptivos en logs: `_logger.info("[Módulo] Mensaje")`.
- Siempre agregar docstrings a funciones y clases.


### Despliegue de Cambios
1. **Cambios en archivos `.py` (modelos/lógica):** Requiere restart del contenedor.
   ```bash
   docker restart odoo-16-hazard-new
   ```
2. **Cambios en estructura de tablas (fields nuevos):** Requiere restart + actualización del módulo.
   ```bash
   docker restart odoo-16-hazard-new
   # Luego ejecutar el script de actualización:
   python3 update_module.py
   ```
3. **Cambios solo en XML/vistas/reportes:** Solo requiere actualización del módulo (sin restart).

### Seguridad y Respaldos
- **NUNCA** ejecutar `UPDATE` o `DELETE` masivos sin respaldo previo.
- Script de respaldo: `backup_hazard.sh` (respalda DB + código, retención 15 días).
- Los backups se suben a Google Drive automáticamente vía Plesk.

### Código de Barras
- Formato EAN13 estandarizado con estructura `20000...` (ej. `2000010012749`).
- NUNCA crear scripts que alteren la secuencia de códigos de barras.

### Archivos que NO se deben modificar
- Módulos en `/mnt/enterprise` (módulos oficiales de Odoo Enterprise).
- Solo modificar código en `/mnt/extra-addons` (módulos custom).

## Scripts Útiles del Proyecto
| Script | Propósito | Comando |
|---|---|---|
| `backup_hazard.sh` | Respaldo completo (DB + código) | `bash backup_hazard.sh` |
| `update_module.py` | Actualizar módulo hazard_shopify via XML-RPC | `python3 update_module.py` |

## Conexión MCP Server PostgreSQL
El agente tiene acceso directo de solo lectura a la base de datos `hazard_new` a través del MCP Server configurado en `mcp_config.json`. Usar la herramienta `query` del servidor `odoo-postgres` para consultas SQL cuando sea necesario inspeccionar datos.

## Flujo Custom y Maquila (B2B)
- **Regla Crítica:** Los productos de las categorías `Insumos / Materia Prima` y `Producto Final Custom` pertenecen al flujo B2B/Maquila. 
- Estos productos **NUNCA** deben enviarse ni sincronizarse con Shopify. Están tecnológicamente aislados de la plataforma de Retail.
- Cualquier script, sincronización o lógica de Odoo que interactúe con Shopify debe excluir explícitamente estas categorías.

---

## Arquitectura del Módulo `hazard_shopify`

### Modelos Custom (13 modelos)

| Modelo Técnico | Archivo | Descripción |
|---|---|---|
| `shopify.config` | `models/shopify_config.py` | Configuración central de la tienda Shopify. Contiene TODA la lógica de sincronización (~4500 líneas). |
| `shopify.sku.simulation.line` | `models/sku_simulation_line.py` | Líneas de simulación de SKU para mapeo Shopify ↔ Odoo. |
| `shopify.stock.queue` | `models/shopify_stock_queue.py` | Cola asíncrona para sincronización reactiva de inventario. |
| `shopify.cost.line` | `models/shopify_cost_line.py` | Líneas de sincronización de costos desde Shopify. |
| `shopify.publication` | `models/shopify_publication.py` | Canales de venta de Shopify (Online Store, POS, etc.). |
| `shopify.product.publication` | `models/shopify_product_publication.py` | Tabla pivote: estado de publicación de un producto por canal. |
| `shopify.location.mapping` | `models/shopify_config.py` (final) | Mapeo Sucursal Shopify ↔ Almacén/Ubicación Odoo. |
| `shopify.location.wizard` | `models/shopify_location_wizard.py` | Wizard para seleccionar sucursal de Shopify. |
| `shopify.refund.wizard` | `wizard/shopify_refund_wizard.py` | Asistente para reembolsos manuales hacia Shopify. |

### Modelos Heredados (overrides)

| Modelo | Archivo | Qué agrega |
|---|---|---|
| `product.template` | `models/product_template.py` | Campos `shopify_id`, `shopify_published`, `shopify_sync_status`, lógica de push a Shopify, canales de venta GraphQL, generación de barcodes. |
| `product.product` | `models/product_template.py` (L960+) | Campos **CRÍTICOS**: `shopify_variant_id` (llave de variante), `shopify_inventory_item_id` (llave de inventario). Auto-generación de EAN-13 al crear producto. |
| `sale.order` | `models/sale_order.py` | Campos `shopify_order_id`, `shopify_fulfillment_status`, `shopify_refund_status`, `shopify_shipping_title`. Lógica BOPIS, fulfillment, cancelación de pickings. |
| `stock.quant` | `models/stock_quant.py` | Override de `_update_available_quantity()` para encolar sincronización reactiva al cambiar stock. |
| `res.users` | `models/res_users.py` | Override de `_search()` para ocultar usuarios de soporte (`admin`, `@tesscorp.com.mx`). |

### Campos Shopify Críticos (NUNCA eliminar)

**En `product.product`:**
- `shopify_variant_id` — ID numérico de la variante en Shopify. Es la llave para push de SKU, barcodes y fulfillment.
- `shopify_inventory_item_id` — ID del ítem de inventario en Shopify. Es la llave para TODA la sincronización de stock.

**En `product.template`:**
- `shopify_id` — ID del producto (template) en Shopify.
- `shopify_published` — Flag booleano que indica si el producto existe en Shopify.

**En `sale.order`:**
- `shopify_order_id` — ID numérico del pedido en Shopify. Llave para duplicados y fulfillment.
- `shopify_shipping_title` — Título del método de envío. Se usa para detectar pedidos BOPIS (retiro en tienda).

---

## Flujos de Negocio del Conector

### Flujo 1: Importación de Pedidos (Shopify → Odoo)
```
Cron cada N min → cron_import_orders()
  → API REST: GET /orders.json?status=any&updated_at_min=...
  → Por cada pedido: _import_single_shopify_order(s_order)
    → Si ya existe: actualiza estado financiero, fulfillment, refunds, detecta ediciones
    → Si es nuevo: crea res.partner → crea sale.order → auto-confirma
      → Si POS o Fulfilled: auto-valida albarán de salida
      → Si tiene envío: agrega línea de producto ENVIO
      → Si pagado y auto_workflow_payment: publica factura + registra pago
      → Si fecha < cutoff: cancela albaranes (protección histórica)
    → Ejecuta _process_shopify_refunds_automation_logic()
```

**Reglas importantes:**
- El producto se busca por: 1° SKU (`default_code`), 2° `shopify_variant_id`, 3° nombre (`ilike`), 4° genérico `SHOPIFY-GENERIC`.
- Los impuestos se omiten (`tax_id: []`) porque Shopify ya los incluye en el precio.
- Los descuentos se calculan como porcentaje desde `discount_allocations`.

### Flujo 2: Sincronización de Inventario (Odoo → Shopify)
```
Odoo es la FUENTE DE VERDAD del stock.

Modo Reactivo (por evento):
  stock.quant._update_available_quantity() → detecta cambio
    → Si producto tiene shopify_inventory_item_id → encola en shopify.stock.queue
    → Cron cada 5 min: process_queue_batch() → procesa 15 productos por lote
    → Llama a _sync_variants_stock_to_shopify()

Modo Cron (masivo periódico):
  cron_sync_inventory_to_shopify() → todas las variantes con shopify_inventory_item_id
    → _sync_variants_stock_to_shopify(variants)
    → API REST: POST /inventory_levels/set.json por cada variante × ubicación
```

**Regla CRÍTICA:** Jamás invertir la dirección. El cron `cron_sync_inventory_from_shopify` está redirigido a `cron_sync_inventory_to_shopify` por seguridad operativa.

### Flujo 3: Fulfillment (Odoo → Shopify)
```
Desde el pedido en Odoo → botón "Enviar a Shopify"
  → action_notify_shopify_fulfillment()
  → Doble fallback:
    1° Intento: Endpoint legacy POST /orders/{id}/fulfillments.json
    2° Intento: Nuevo endpoint POST /fulfillments.json via fulfillment_orders
  → Escribe shopify_fulfillment_id y shopify_fulfillment_status en sale.order
```

### Flujo 4: BOPIS (Buy Online, Pickup In Store)
```
Detección automática por shopify_shipping_title:
  Si contiene: OFICINA, RETIRO, PICKUP, SUCURSAL, TIENDA
    → Al confirmar pedido: _create_pickup_internal_transfer()
      → Crea traslado interno: Bodega ALMA → Oficina HRSGOLF
      → Reserva stock y alerta si hay faltantes
    → Al editar pedido en Shopify: _update_pickup_internal_transfer_qty()
      → Ajusta cantidades en traslados existentes o crea complementarios
```

### Flujo 5: Reembolsos y Devoluciones Automáticas
```
_process_shopify_refunds_automation_logic(sale_order, s_order)
  → Candado Sandbox: Si shopify_sandbox_mode=True, solo procesa SKUs con "PRUEBA"
  → Por cada refund no procesado:
    A. Devolución de Stock: Crea stock.picking de entrada + auto-valida
    B. Nota de Crédito: Crea account.move tipo out_refund en borrador
  → Registra refund_id en shopify_refund_ids_processed para idempotencia
```

**ADVERTENCIA:** Este flujo ya opera en producción. Cambios aquí impactan contabilidad real y stock físico.

### Flujo 6: Publicación de Productos (Odoo → Shopify)
```
Desde ficha de producto → botón "Publicar en Shopify"
  → action_push_to_shopify()
    → Si shopify_id existe: PUT /products/{id}.json (actualización)
    → Si no existe: POST /products.json (creación)
  → Sincroniza canales de venta vía API GraphQL (publishablePublish mutation)
  → Sube imagen si tiene (POST /products/{id}/images.json)
```

---

## Sistema de Crons (7 Acciones Planificadas)

| # | XML ID | Propósito | Frecuencia Default | Activo | Método |
|---|---|---|---|---|---|
| 1 | `ir_cron_shopify_import_orders` | Importar pedidos Shopify → Odoo | 5 min | ✅ | `cron_import_orders()` |
| 2 | `ir_cron_shopify_sync_inventory` | Stock Odoo → Shopify (masivo) | 12 horas | ❌ | `cron_sync_inventory_to_shopify()` |
| 3 | `ir_cron_shopify_refresh_tokens` | Renovar tokens OAuth (24h Shopify) | 20 horas | ✅ | `cron_refresh_tokens()` |
| 4 | `ir_cron_shopify_download_images` | Descargar imágenes (manual/batch) | 1 min | ❌ | `_cron_download_images_batch()` |
| 5 | `ir_cron_shopify_auto_image_sync` | Auto-descargar imágenes nuevas | 2 horas | ✅ | `cron_auto_download_images()` |
| 6 | `ir_cron_shopify_process_stock_queue` | Procesar cola de stock reactiva | 5 min | ✅ | `process_queue_batch()` |
| 7 | `ir_cron_shopify_clean_stock_queue` | Limpiar cola completada (+7 días) | 1 día | ✅ | `clean_done_queue()` |

**Reglas sobre Crons:**
- Los crons 1, 2 y 5 se actualizan dinámicamente desde el `write()` de `shopify.config` cuando el usuario cambia la frecuencia en la interfaz.
- **NUNCA** desactivar manualmente los crons 3 (tokens) o 6 (cola de stock) — rompe la operación.
- El cron 4 se activa/desactiva automáticamente (botón → se activa, termina → se desactiva).

---

## API de Shopify

### Configuración Técnica
- **Versión API REST:** `2024-01`
- **Versión API GraphQL:** `2024-01`
- **Autenticación:** Token de acceso (`shpat_...`) en header `X-Shopify-Access-Token`
- **Renovación de Token:** OAuth con `client_id` y `client_secret`, renovación automática cada 20h vía cron.
- **Rate Limiting:** `time.sleep(0.5)` entre requests (2 req/s). Reintentos automáticos en HTTP 429 (espera 5s, 3 intentos).

### Endpoints Usados
| Método | Endpoint | Propósito |
|---|---|---|
| GET | `/orders.json` | Importar pedidos |
| GET | `/products.json` | Importar catálogo |
| POST/PUT | `/products/{id}.json` | Crear/actualizar productos |
| POST | `/products/{id}/images.json` | Subir imágenes |
| POST | `/inventory_levels/set.json` | Sincronizar stock |
| GET | `/locations.json` | Obtener sucursales |
| POST | `/orders/{id}/fulfillments.json` | Fulfillment legacy |
| GET | `/orders/{id}/fulfillment_orders.json` | Fulfillment orders (nuevo) |
| POST | `/fulfillments.json` | Fulfillment nuevo API |
| POST | `/orders/{id}/refunds/calculate.json` | Calcular reembolso |
| POST | `/orders/{id}/refunds.json` | Ejecutar reembolso |
| PUT | `/variants/{id}.json` | Actualizar variante (SKU, barcode) |
| GET | `/inventory_levels.json` | Consultar niveles de stock |
| POST (GraphQL) | `/graphql.json` | Costos, canales de venta, publicaciones |

---

## Grupos de Seguridad del Módulo

Definidos en `security/hazard_security.xml`. Categoría: `Hazard / HRSGOLF`.

| XML ID | Nombre | Permisos | Hereda de |
|---|---|---|---|
| `group_hazard_designer` | Diseño | Crea productos, sube fotos. Sin precios ni Shopify. | `base.group_user` |
| `group_hazard_warehouse` | Bodega | Stock y tracking. Sin precios. | `stock.group_stock_user` |
| `group_hazard_sales` | Ventas B2B | Cotizaciones/pedidos. Precios solo lectura. | `sales_team.group_sale_salesman` |
| `group_hazard_finance` | Admin/Finanzas | Control total: precios, catálogo, Shopify. | `sales_team.group_sale_salesman_all_leads` |

**Regla:** Solo `group_hazard_finance` tiene acceso de escritura a `shopify.config`. Los demás tienen solo lectura o sin acceso.

---

## Cola Asíncrona de Stock (`shopify.stock.queue`)

Mecanismo reactivo para sincronizar inventario en tiempo semi-real:

1. **Trigger:** `stock.quant._update_available_quantity()` detecta cualquier cambio de stock en Odoo.
2. **Filtro:** Solo encola productos con `shopify_inventory_item_id` (vinculados a Shopify).
3. **Encolado:** Crea registro en `shopify.stock.queue` con estado `pending` (evita duplicados).
4. **Procesamiento:** Cron cada 5 min → `process_queue_batch()` → procesa 15 productos por lote.
5. **Limpieza:** Cron diario → `clean_done_queue()` → elimina registros `done` con más de 7 días.

**Condición:** Solo procesa si `auto_sync_products = True` en la configuración de Shopify.

---

## Lógica de SKU (`sku_logic.py`)

Formato estándar: `HRS-{TIPO}-{MODELO}-{GENERO}-{COLOR}-{TALLA}`

Ejemplo: `HRS-POL-CLUB-M-BLK-L` = Polo Club, Hombre, Negro, Talla L

### Diccionarios de Mapeo (hardcoded)
- **CATEGORIES:** `hat→HAT`, `polo→POL`, `hoodie→HOD`, `bag→BAG`, `glove→GLV`, `towel→TWL`, etc.
- **GENDERS:** `men→M`, `women→W`, `kids→K`. Default `U` (unisex) para accesorios.
- **COLORS:** `black→BLK`, `white→WHT`, `green→GRN`, `blue→BLU`, `red→RED`, etc.

**Regla:** NUNCA modificar estos diccionarios sin aprobación explícita del cliente. Un cambio rompe la consistencia de los SKUs existentes.

---

## Reglas de Seguridad Adicionales

### Override de Usuarios (`res_users.py`)
- El modelo `res.users` oculta los usuarios de soporte (`admin` y `@tesscorp.com.mx`) de los usuarios del cliente.
- **NUNCA eliminar este override.** Los usuarios del cliente no deben ver las cuentas de desarrollo.

### Sandbox de Reembolsos
- El campo `shopify_sandbox_mode` en `shopify.config` protege la base de datos de producción.
- Cuando está activo (por defecto), la automatización de reembolsos solo procesa pedidos con SKUs que contienen la palabra `PRUEBA`.
- **NUNCA desactivar este modo sin autorización explícita y pruebas exhaustivas.**

### Producto Genérico de Envío
- El producto con SKU `ENVIO` (tipo servicio) se usa para las líneas de costo de envío importadas de Shopify.
- Se crea automáticamente si no existe. **No eliminar.**

### Cliente Genérico POS
- El contacto `Cliente POS Shopify` se reutiliza para ventas POS sin datos de cliente.
- **No eliminar ni renombrar.** Evita crear contactos basura por cada venta POS.

### Log Persistente de Crons
- Ruta dentro del contenedor: `/var/lib/odoo/shopify_cron.log`
- Volumen Docker: `odoo-web-data-hazard-new`
- Máximo 5 MB con rotación automática.
- Para consultar: `docker exec odoo-16-hazard-new cat /var/lib/odoo/shopify_cron.log`

### Fecha de Corte de Importación
- El campo `shopify_import_cutoff_date` en `shopify.config` protege el inventario de pedidos históricos.
- Pedidos creados ANTES de esta fecha se importan pero sus albaranes de salida se cancelan automáticamente.
- **No modificar esta fecha sin entender el impacto en el stock.**

---

## Autorización de Borrado
- NUNCA elimines archivos, directorios o recursos proactivamente sin la autorización explícita y directa del usuario.
- Si el usuario pregunta "¿Puedo borrar esto?", "¿Es seguro borrar esto?" o similar, limítate EXCLUSIVAMENTE a responder a su pregunta proporcionando la información.
- NO asumas que el usuario quiere que tú ejecutes el comando de borrado. Espera a que el usuario diga explícitamente "Bórralo", "Por favor elimínalo tú", o que ejecute la acción él mismo.

## Limpieza y Scripts Temporales
- NUNCA crees scripts temporales de Python (o bash) directamente en la carpeta raíz del proyecto.
- Si necesitas crear un script (`.py`, `.sh`, etc.) para una tarea, colócalo SIEMPRE dentro del directorio `scripts/`. Crea este directorio si no existe.

