# 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.