Jelajahi Sumber

Squashed 'whatsapp_web/' changes from c5dd4a0..0135327

0135327 Merge remote-tracking branch 'origin/develop' into develop
acbf0d5 update status mensaje
1720df3 fix: corregir sintaxis de herencia y orden de carga de modelos
b530bc8 fix: agregar archivo mail_message.py faltante al repositorio
dc44d89 refactor: eliminar dependencia circular - remover campos Many2one a ww.group
deb2f03 Migrar a nueva API Gateway: usar session_name y api_key, endpoint groups desde BD
06d6e7a Add API key and login fields, add .gitignore
b94917a Add comprehensive WhatsApp Web integration features
cf6a1a2 fix mobile send whatsapp web
a4e54cf fix mobile
af86bc3 log whatsapp web json
ae80709 Merge branch 'develop' of ssh://g1t.m22.mx:12765/M22/whatsapp_web into develop
9fea7d2 nueva funcion para integracion con whatsapp web groups
334bbe3 Merge tag 'v0.1' into develop
REVERT: c5dd4a0 fix field verification
REVERT: fdfef03 fix
REVERT: 5c943c4 fix

git-subtree-dir: whatsapp_web
git-subtree-split: 0135327dc1e03021a1849e2595d5753107e3fb56
odoo 2 bulan lalu
induk
melakukan
af69268105

+ 53 - 0
.gitignore

@@ -0,0 +1,53 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Odoo
+*.pot
+*.mo
+*.log
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+.DS_Store
+
+# Environment
+.env
+.venv
+env/
+venv/
+ENV/
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+
+# Temporary files
+*.tmp
+*.bak
+*.orig
+
+

+ 466 - 0
API_REFERENCE.md

@@ -0,0 +1,466 @@
+# API Reference - WhatsApp Web Integration
+
+## Índice
+- [Modelos](#modelos)
+- [Métodos](#métodos)
+- [Campos](#campos)
+- [Ejemplos de Uso](#ejemplos-de-uso)
+- [Respuestas de API](#respuestas-de-api)
+
+## Modelos
+
+### whatsapp.account (Extendido)
+
+Extensión del modelo base de cuentas WhatsApp para soportar WhatsApp Web.
+
+#### Campos Adicionales
+
+| Campo | Tipo | Descripción |
+|-------|------|-------------|
+| `whatsapp_web_url` | Char | URL del servidor whatsapp-web.js |
+
+#### Métodos
+
+##### `get_groups()`
+Obtiene la lista de grupos disponibles en WhatsApp Web.
+
+**Parámetros:** Ninguno
+
+**Retorna:** `list` - Lista de diccionarios con información de grupos
+
+**Ejemplo:**
+```python
+account = self.env['whatsapp.account'].browse(1)
+groups = account.get_groups()
+```
+
+**Respuesta esperada:**
+```python
+[
+    {
+        'id': {'_serialized': '120363158956331133@g.us'},
+        'name': 'Mi Grupo',
+        'members': [
+            {
+                'id': {'_serialized': '5215551234567@c.us'},
+                'number': '5551234567',
+                'name': 'Juan Pérez',
+                'isAdmin': False,
+                'isSuperAdmin': False
+            }
+        ]
+    }
+]
+```
+
+---
+
+### whatsapp.message (Extendido)
+
+Extensión del modelo base de mensajes WhatsApp para soportar grupos.
+
+#### Campos Adicionales
+
+| Campo | Tipo | Descripción |
+|-------|------|-------------|
+| `recipient_type` | Selection | Tipo de destinatario: 'phone' o 'group' |
+| `whatsapp_group_id` | Many2one | Referencia al grupo (modelo ww.group) |
+| `final_recipient` | Char | Destinatario final calculado |
+
+#### Métodos
+
+##### `_compute_final_recipient()`
+Calcula el destinatario final basado en el tipo de destinatario.
+
+**Parámetros:** Ninguno (compute method)
+
+**Retorna:** `str` - ID del destinatario final
+
+##### `_get_final_destination()`
+Método mejorado para obtener destino final (grupo o teléfono).
+
+**Parámetros:** Ninguno
+
+**Retorna:** `str|False` - ID del destino final o False
+
+**Ejemplo:**
+```python
+message = self.env['whatsapp.message'].browse(1)
+destination = message._get_final_destination()
+```
+
+##### `_send_message(with_commit=False)`
+Override del método de envío para soportar WhatsApp Web.
+
+**Parámetros:**
+- `with_commit` (bool): Si hacer commit después del envío
+
+**Retorna:** `None`
+
+**Ejemplo:**
+```python
+message = self.env['whatsapp.message'].browse(1)
+message._send_message(with_commit=True)
+```
+
+---
+
+### whatsapp.composer (Extendido)
+
+Extensión del composer de WhatsApp para soportar grupos y mensajes libres.
+
+#### Campos Adicionales
+
+| Campo | Tipo | Descripción |
+|-------|------|-------------|
+| `recipient_type` | Selection | Tipo de destinatario: 'phone' o 'group' |
+| `whatsapp_group_id` | Many2one | Referencia al grupo |
+| `whatsapp_group_id_char` | Char | ID de grupo manual |
+| `body` | Html | Mensaje libre (sin plantilla) |
+
+#### Métodos
+
+##### `_check_recipient_configuration()`
+Valida la configuración del destinatario en el composer.
+
+**Parámetros:** Ninguno (constraint method)
+
+**Retorna:** `None` (raise ValidationError si hay error)
+
+##### `_compute_invalid_phone_number_count()`
+Override para casos específicos de grupos.
+
+**Parámetros:** Ninguno (compute method)
+
+**Retorna:** `None`
+
+##### `_onchange_recipient_type()`
+Limpia campos al cambiar tipo de destinatario.
+
+**Parámetros:** Ninguno (onchange method)
+
+**Retorna:** `None`
+
+##### `action_send_whatsapp_template()`
+Override del método de envío para casos específicos de WhatsApp Web.
+
+**Parámetros:** Ninguno
+
+**Retorna:** `dict` - Acción de ventana o resultado del envío
+
+##### `_send_whatsapp_web_message()`
+Envía mensaje WhatsApp Web sin plantilla.
+
+**Parámetros:** Ninguno
+
+**Retorna:** `dict` - Acción de cierre de ventana
+
+**Ejemplo:**
+```python
+composer = self.env['whatsapp.composer'].create({
+    'recipient_type': 'group',
+    'whatsapp_group_id': group.id,
+    'body': 'Hola grupo!'
+})
+result = composer._send_whatsapp_web_message()
+```
+
+##### `_prepare_whatsapp_message_values(record)`
+Override para agregar información de grupo.
+
+**Parámetros:**
+- `record` (recordset): Registro relacionado
+
+**Retorna:** `dict` - Valores para crear mensaje WhatsApp
+
+##### `_get_whatsapp_web_account()`
+Obtiene cuenta de WhatsApp Web disponible.
+
+**Parámetros:** Ninguno
+
+**Retorna:** `whatsapp.account` - Cuenta WhatsApp Web
+
+**Ejemplo:**
+```python
+composer = self.env['whatsapp.composer'].create({})
+account = composer._get_whatsapp_web_account()
+```
+
+##### `_send_whatsapp_message_without_template(body, phone=None, group_id=None)`
+Envía mensaje de WhatsApp sin plantilla.
+
+**Parámetros:**
+- `body` (str): Contenido del mensaje
+- `phone` (str, opcional): Número de teléfono
+- `group_id` (str, opcional): ID del grupo
+
+**Retorna:** `whatsapp.message` - Mensaje creado
+
+**Ejemplo:**
+```python
+composer = self.env['whatsapp.composer'].create({})
+message = composer._send_whatsapp_message_without_template(
+    body="Mensaje de prueba",
+    group_id="120363158956331133@g.us"
+)
+```
+
+---
+
+## Ejemplos de Uso
+
+### Envío de Mensaje Individual
+
+```python
+# Crear composer para número individual
+composer = self.env['whatsapp.composer'].create({
+    'recipient_type': 'phone',
+    'phone': '+525551234567',
+    'wa_template_id': template.id,
+    'res_model': 'res.partner',
+    'res_id': partner.id
+})
+
+# Enviar mensaje
+result = composer.action_send_whatsapp_template()
+```
+
+### Envío de Mensaje a Grupo
+
+```python
+# Crear composer para grupo
+composer = self.env['whatsapp.composer'].create({
+    'recipient_type': 'group',
+    'whatsapp_group_id': group.id,
+    'body': '<p>Hola grupo! Este es un mensaje libre.</p>',
+    'res_model': 'ww.group',
+    'res_id': group.id
+})
+
+# Enviar mensaje
+result = composer.action_send_whatsapp_template()
+```
+
+### Envío con Adjunto
+
+```python
+# Crear attachment
+attachment = self.env['ir.attachment'].create({
+    'name': 'documento.pdf',
+    'type': 'binary',
+    'datas': base64.b64encode(pdf_content),
+    'mimetype': 'application/pdf'
+})
+
+# Crear composer con adjunto
+composer = self.env['whatsapp.composer'].create({
+    'recipient_type': 'phone',
+    'phone': '+525551234567',
+    'body': 'Adjunto documento importante',
+    'attachment_id': attachment.id,
+    'res_model': 'res.partner',
+    'res_id': partner.id
+})
+
+# Enviar mensaje
+result = composer.action_send_whatsapp_template()
+```
+
+### Obtener Grupos de WhatsApp Web
+
+```python
+# Obtener cuenta WhatsApp Web
+account = self.env['whatsapp.account'].search([
+    ('whatsapp_web_url', '!=', False)
+], limit=1)
+
+# Obtener grupos
+groups = account.get_groups()
+
+# Procesar grupos
+for group_data in groups:
+    group_id = group_data['id']['_serialized']
+    group_name = group_data['name']
+    members = group_data.get('members', [])
+    
+    print(f"Grupo: {group_name} ({group_id})")
+    print(f"Miembros: {len(members)}")
+```
+
+### Envío Directo de Mensaje
+
+```python
+# Crear mensaje directamente
+message = self.env['whatsapp.message'].create({
+    'recipient_type': 'group',
+    'whatsapp_group_id': group.id,
+    'mobile_number': group.whatsapp_web_id,
+    'body': 'Mensaje directo al grupo',
+    'wa_account_id': account.id,
+    'state': 'outgoing'
+})
+
+# Enviar mensaje
+message._send_message(with_commit=True)
+```
+
+## Respuestas de API
+
+### Respuesta Exitosa de Envío
+
+```json
+{
+    "_data": {
+        "id": {
+            "_serialized": "3EB0C767D26A3D1B7B4A_5215551234567@c.us"
+        }
+    }
+}
+```
+
+### Respuesta de Grupos
+
+```json
+[
+    {
+        "id": {
+            "_serialized": "120363158956331133@g.us"
+        },
+        "name": "Mi Grupo de Trabajo",
+        "description": "Grupo para coordinación de proyectos",
+        "members": [
+            {
+                "id": {
+                    "_serialized": "5215551234567@c.us"
+                },
+                "number": "5551234567",
+                "name": "Juan Pérez",
+                "pushname": "Juan",
+                "isAdmin": true,
+                "isSuperAdmin": false
+            },
+            {
+                "id": {
+                    "_serialized": "5215557654321@c.us"
+                },
+                "number": "5557654321",
+                "name": "María García",
+                "pushname": "María",
+                "isAdmin": false,
+                "isSuperAdmin": false
+            }
+        ],
+        "messages": [
+            {
+                "id": {
+                    "_serialized": "3EB0C767D26A3D1B7B4A"
+                },
+                "body": "Hola grupo!",
+                "author": "5215551234567@c.us",
+                "timestamp": 1640995200,
+                "hasQuotedMsg": false
+            }
+        ]
+    }
+]
+```
+
+### Respuesta de Error
+
+```json
+{
+    "error": "Método no implementado",
+    "code": 501
+}
+```
+
+## Códigos de Estado HTTP
+
+| Código | Descripción |
+|--------|-------------|
+| 200 | Petición exitosa |
+| 400 | Error en los parámetros |
+| 401 | No autorizado |
+| 404 | Endpoint no encontrado |
+| 500 | Error interno del servidor |
+| 501 | Método no implementado |
+
+## Formato de Números
+
+### Números Individuales
+- **Formato de entrada:** `+525551234567`, `5551234567`, `5215551234567`
+- **Formato interno:** `5215551234567@c.us`
+- **Reglas de formateo:**
+  - Se eliminan espacios, guiones y el símbolo +
+  - Si empieza con "52" y tiene 12 dígitos, se agrega "1"
+  - Si tiene 10 dígitos, se agrega "521"
+  - Se agrega sufijo "@c.us"
+
+### Grupos
+- **Formato:** `120363158956331133@g.us`
+- **Identificación:** Termina en "@g.us"
+- **No se aplica formateo:** Se usa tal como viene de WhatsApp Web
+
+## Manejo de Errores
+
+### Excepciones Comunes
+
+#### ValidationError
+```python
+from odoo.exceptions import ValidationError
+
+try:
+    composer.action_send_whatsapp_template()
+except ValidationError as e:
+    print(f"Error de validación: {e}")
+```
+
+#### ConnectionError
+```python
+import requests
+
+try:
+    response = requests.post(url, data=payload, headers=headers)
+except requests.exceptions.ConnectionError:
+    print("Error de conexión con el servidor WhatsApp Web")
+```
+
+### Logs de Debugging
+
+```python
+import logging
+_logger = logging.getLogger(__name__)
+
+# Log de envío exitoso
+_logger.info('WHATSAPP WEB SEND MESSAGE: %s', url)
+
+# Log de error
+_logger.error("Error en la petición de groups: %s", response.text)
+```
+
+## Configuración de Timeouts
+
+```python
+# Configurar timeout para requests
+import requests
+
+response = requests.post(
+    url, 
+    data=payload, 
+    headers=headers,
+    timeout=30  # 30 segundos
+)
+```
+
+## Rate Limiting
+
+El módulo incluye delays aleatorios entre envíos:
+
+```python
+import time
+import random
+
+# Delay aleatorio entre 3-7 segundos
+time.sleep(random.randint(3, 7))
+```
+

+ 225 - 0
README.md

@@ -0,0 +1,225 @@
+# WhatsApp Web Integration for Odoo 18
+
+## Descripción
+
+Este módulo integra WhatsApp con la automatización de marketing de Odoo, permitiendo enviar mensajes de WhatsApp a través de una URL personalizada usando whatsapp-web.js. Proporciona funcionalidad completa para enviar mensajes tanto a números telefónicos como a grupos de WhatsApp.
+
+## Características Principales
+
+- ✅ Envío de mensajes a números telefónicos individuales
+- ✅ Envío de mensajes a grupos de WhatsApp
+- ✅ Soporte para plantillas de WhatsApp Business API
+- ✅ Mensajes de texto libre (sin plantilla) para WhatsApp Web
+- ✅ Gestión de adjuntos y archivos multimedia
+- ✅ Respuestas a mensajes (quoted messages)
+- ✅ Integración con el sistema de marketing de Odoo
+- ✅ Configuración flexible de cuentas WhatsApp Web
+
+## Requisitos
+
+- Odoo 18.0
+- Módulo `whatsapp` (dependencia)
+- Servidor whatsapp-web.js configurado y funcionando
+
+## Instalación
+
+1. **Instalar el módulo:**
+   ```bash
+   cd /var/odoo/mcteam.run
+   sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -i whatsapp_web
+   ```
+
+2. **Reiniciar el servidor Odoo:**
+   ```bash
+   ./restart_odoo.sh
+   ```
+
+## Configuración
+
+### 1. Configuración General
+
+1. Ir a **Configuración > Técnico > Parámetros del Sistema**
+2. Buscar el parámetro `whatsapp_web.whatsapp_endpoint`
+3. Establecer la URL de tu servidor whatsapp-web.js (ej: `https://web.whatsapp.com/api`)
+
+### 2. Configuración de Cuenta WhatsApp
+
+1. Ir a **WhatsApp > Configuración > Cuentas WhatsApp**
+2. Crear o editar una cuenta WhatsApp
+3. En la sección "WhatsApp Web", configurar:
+   - **WhatsApp Web URL**: URL de tu servidor whatsapp-web.js
+4. Guardar la configuración
+
+### 3. Configuración de Grupos (Opcional)
+
+Si deseas enviar mensajes a grupos, instala también el módulo `whatsapp_web_groups`:
+
+```bash
+sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -i whatsapp_web_groups
+```
+
+## Uso
+
+### Envío de Mensajes Individuales
+
+1. **Desde el Composer de WhatsApp:**
+   - Seleccionar "Phone Number" como tipo de destinatario
+   - Ingresar el número de teléfono
+   - Elegir una plantilla o escribir mensaje libre
+   - Enviar
+
+2. **Desde Marketing:**
+   - Crear una campaña de marketing
+   - Configurar plantillas WhatsApp
+   - Ejecutar campaña
+
+### Envío de Mensajes a Grupos
+
+1. **Desde el Composer:**
+   - Seleccionar "WhatsApp Group" como tipo de destinatario
+   - Elegir un grupo de la lista o ingresar ID manualmente
+   - Escribir mensaje libre o usar plantilla
+   - Enviar
+
+2. **Desde la vista de Grupos:**
+   - Ir a **WhatsApp Web > Grupos**
+   - Seleccionar un grupo
+   - Hacer clic en "Send WhatsApp Message"
+
+### Formato de Números
+
+- **Números individuales**: Se formatean automáticamente con prefijo de país (ej: `5215551234567@c.us`)
+- **Grupos**: Usan formato `120363158956331133@g.us`
+
+### Mensajes con Adjuntos
+
+El módulo soporta envío de:
+- Documentos PDF
+- Imágenes
+- Videos
+- Audio
+- Cualquier tipo de archivo soportado por WhatsApp
+
+## API del Módulo
+
+### Métodos Principales
+
+#### `whatsapp.account.get_groups()`
+Obtiene la lista de grupos disponibles en WhatsApp Web.
+
+```python
+account = self.env['whatsapp.account'].browse(1)
+groups = account.get_groups()
+```
+
+#### `whatsapp.message._send_message()`
+Envía un mensaje WhatsApp usando la configuración de la cuenta.
+
+#### `whatsapp.composer.action_send_whatsapp_template()`
+Procesa el envío desde el composer, soportando tanto plantillas como mensajes libres.
+
+## Estructura de Datos
+
+### Modelos Principales
+
+#### `whatsapp.account` (Extendido)
+- `whatsapp_web_url`: URL del servidor whatsapp-web.js
+
+#### `whatsapp.message` (Extendido)
+- `recipient_type`: Tipo de destinatario ('phone' o 'group')
+- `whatsapp_group_id`: Referencia al grupo (si aplica)
+- `final_recipient`: Destinatario final calculado
+
+#### `whatsapp.composer` (Extendido)
+- `recipient_type`: Tipo de destinatario
+- `whatsapp_group_id`: Grupo seleccionado
+- `whatsapp_group_id_char`: ID de grupo manual
+- `body`: Mensaje libre (sin plantilla)
+
+## Configuración del Servidor whatsapp-web.js
+
+### Ejemplo de Configuración
+
+```javascript
+// Ejemplo de servidor básico
+const express = require('express');
+const { Client } = require('whatsapp-web.js');
+
+const app = express();
+app.use(express.json());
+
+const client = new Client();
+
+client.on('ready', () => {
+    console.log('WhatsApp Web está listo');
+});
+
+// Endpoint para enviar mensajes
+app.post('/', async (req, res) => {
+    const { method, args } = req.body;
+    
+    if (method === 'sendMessage') {
+        const [to, content, options] = args;
+        const message = await client.sendMessage(to, content, options);
+        res.json({ _data: { id: { _serialized: message.id._serialized } } });
+    }
+});
+
+client.initialize();
+app.listen(3000);
+```
+
+## Solución de Problemas
+
+### Error: "No WhatsApp Web account configured"
+- Verificar que la cuenta WhatsApp tenga configurada la URL de WhatsApp Web
+- Confirmar que la URL sea accesible desde el servidor Odoo
+
+### Error: "Error en la petición de groups"
+- Verificar conectividad con el servidor whatsapp-web.js
+- Confirmar que el servidor esté ejecutándose y respondiendo
+- Revisar logs del servidor para errores específicos
+
+### Mensajes no se envían
+- Verificar que WhatsApp Web esté conectado en el servidor
+- Confirmar que el número de teléfono esté en formato correcto
+- Revisar logs de Odoo para errores de la API
+
+### Grupos no aparecen
+- Ejecutar sincronización manual desde el menú de grupos
+- Verificar que el método `getGroups` esté implementado en el servidor
+- Confirmar permisos de lectura de grupos en WhatsApp
+
+## Logs y Debugging
+
+Los logs se pueden encontrar en:
+- **Logs de Odoo**: `/var/odoo/stg2.mcteam.run/logs/odoo-server.log`
+- **Logs del servidor whatsapp-web.js**: Revisar configuración del servidor
+
+Para debugging, buscar en los logs:
+- `WHATSAPP WEB SEND MESSAGE`
+- `Error en la petición`
+- `Petición exitosa`
+
+## Actualizaciones
+
+Para actualizar el módulo:
+
+```bash
+cd /var/odoo/mcteam.run
+sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -u whatsapp_web
+./restart_odoo.sh
+```
+
+## Soporte
+
+Para soporte técnico o reportar bugs, contactar al equipo de desarrollo.
+
+## Changelog
+
+### Versión 1.0
+- Implementación inicial
+- Soporte para mensajes individuales y grupos
+- Integración con whatsapp-web.js
+- Soporte para plantillas y mensajes libres
+

+ 661 - 0
TROUBLESHOOTING.md

@@ -0,0 +1,661 @@
+# Guía de Solución de Problemas - WhatsApp Web Integration
+
+## Índice
+- [Problemas de Configuración](#problemas-de-configuración)
+- [Problemas de Conexión](#problemas-de-conexión)
+- [Problemas de Envío de Mensajes](#problemas-de-envío-de-mensajes)
+- [Problemas de Grupos](#problemas-de-grupos)
+- [Problemas de Sincronización](#problemas-de-sincronización)
+- [Problemas de Rendimiento](#problemas-de-rendimiento)
+- [Logs y Debugging](#logs-y-debugging)
+
+## Problemas de Configuración
+
+### Error: "No WhatsApp Web account configured"
+
+**Síntomas:**
+- Mensaje de error al intentar enviar mensajes
+- No se pueden configurar grupos
+
+**Causas:**
+1. No se ha configurado la URL de WhatsApp Web en la cuenta
+2. La cuenta WhatsApp no tiene el campo `whatsapp_web_url` configurado
+3. El módulo `whatsapp_web` no está instalado
+
+**Soluciones:**
+```bash
+# 1. Verificar instalación del módulo
+cd /var/odoo/mcteam.run
+sudo -u odoo venv/bin/python3 src/odoo-bin shell -c odoo.conf
+
+# En el shell de Odoo:
+env['ir.module.module'].search([('name', '=', 'whatsapp_web')]).state
+```
+
+```python
+# 2. Verificar configuración de cuenta
+account = env['whatsapp.account'].search([], limit=1)
+print(f"URL configurada: {account.whatsapp_web_url}")
+```
+
+```bash
+# 3. Configurar cuenta manualmente desde base de datos
+su - postgres -c "psql stg2.mcteam.run -c \"UPDATE whatsapp_account SET whatsapp_web_url = 'https://tu-servidor.com/api' WHERE id = 1;\" | cat"
+```
+
+### Error: "Template is required for WhatsApp Business API"
+
+**Síntomas:**
+- No se pueden enviar mensajes libres (sin plantilla)
+- Error al usar composer sin plantilla
+
+**Causas:**
+1. No hay cuentas WhatsApp Web configuradas
+2. Se está intentando usar mensaje libre con API oficial
+
+**Soluciones:**
+```python
+# Verificar cuentas WhatsApp Web disponibles
+accounts = env['whatsapp.account'].search([('whatsapp_web_url', '!=', False)])
+print(f"Cuentas WhatsApp Web: {len(accounts)}")
+
+# Si no hay cuentas, configurar una
+account = env['whatsapp.account'].search([], limit=1)
+account.write({'whatsapp_web_url': 'https://tu-servidor.com/api'})
+```
+
+### Error: "Invalid recipient configuration"
+
+**Síntomas:**
+- Error al seleccionar destinatario en composer
+- Validación falla al enviar mensajes
+
+**Causas:**
+1. Tipo de destinatario no coincide con datos proporcionados
+2. Falta información requerida (teléfono o grupo)
+
+**Soluciones:**
+```python
+# Verificar configuración de destinatario
+composer = env['whatsapp.composer'].browse(composer_id)
+print(f"Tipo destinatario: {composer.recipient_type}")
+print(f"Teléfono: {composer.phone}")
+print(f"Grupo: {composer.whatsapp_group_id}")
+
+# Corregir configuración
+if composer.recipient_type == 'group' and not composer.whatsapp_group_id:
+    # Seleccionar grupo o cambiar a teléfono
+    composer.write({'recipient_type': 'phone'})
+```
+
+## Problemas de Conexión
+
+### Error: "Connection refused" o timeout
+
+**Síntomas:**
+- Timeouts en peticiones HTTP
+- Error de conexión al servidor whatsapp-web.js
+
+**Diagnóstico:**
+```bash
+# 1. Verificar conectividad desde servidor Odoo
+curl -X POST https://tu-servidor-whatsapp.com/api \
+  -H "Content-Type: application/json" \
+  -d '{"method": "ping", "args": []}'
+
+# 2. Verificar logs del servidor whatsapp-web.js
+tail -f /ruta/logs/whatsapp-web.log
+```
+
+**Soluciones:**
+```python
+# 1. Verificar URL en configuración
+account = env['whatsapp.account'].search([('whatsapp_web_url', '!=', False)], limit=1)
+print(f"URL configurada: {account.whatsapp_web_url}")
+
+# 2. Probar conectividad desde Odoo
+import requests
+try:
+    response = requests.post(account.whatsapp_web_url, 
+                           json={"method": "ping", "args": []}, 
+                           timeout=10)
+    print(f"Respuesta: {response.status_code}")
+except Exception as e:
+    print(f"Error de conexión: {e}")
+```
+
+### Error: "Invalid SSL certificate"
+
+**Síntomas:**
+- Error SSL al conectar con servidor HTTPS
+- Certificado no válido
+
+**Soluciones:**
+```python
+# Deshabilitar verificación SSL (solo para desarrollo)
+import requests
+requests.packages.urllib3.disable_warnings()
+
+response = requests.post(url, json=payload, verify=False)
+```
+
+```bash
+# Actualizar certificados del sistema
+sudo apt-get update
+sudo apt-get install ca-certificates
+```
+
+### Error: "Method not implemented"
+
+**Síntomas:**
+- El servidor responde pero no implementa métodos requeridos
+- Error 501 en peticiones
+
+**Diagnóstico:**
+```bash
+# Verificar métodos implementados en el servidor
+curl -X POST https://tu-servidor.com/api \
+  -H "Content-Type: application/json" \
+  -d '{"method": "getGroups", "args": []}'
+```
+
+**Soluciones:**
+```javascript
+// Implementar método getGroups en servidor whatsapp-web.js
+app.post('/', async (req, res) => {
+    const { method, args } = req.body;
+    
+    if (method === 'getGroups') {
+        try {
+            const chats = await client.getChats();
+            const groups = chats.filter(chat => chat.isGroup);
+            res.json(groups);
+        } catch (error) {
+            res.status(500).json({error: error.message});
+        }
+    }
+});
+```
+
+## Problemas de Envío de Mensajes
+
+### Error: "Message not sent"
+
+**Síntomas:**
+- Mensajes quedan en estado "outgoing"
+- No se reciben mensajes en WhatsApp
+
+**Diagnóstico:**
+```python
+# Verificar estado de mensajes
+messages = env['whatsapp.message'].search([
+    ('state', '=', 'outgoing'),
+    ('create_date', '>', '2024-01-01')
+])
+for msg in messages:
+    print(f"Mensaje {msg.id}: {msg.state} - {msg.mobile_number}")
+```
+
+**Soluciones:**
+```python
+# 1. Verificar configuración de cuenta
+message = env['whatsapp.message'].browse(message_id)
+print(f"Cuenta: {message.wa_account_id.name}")
+print(f"URL: {message.wa_account_id.whatsapp_web_url}")
+
+# 2. Reintentar envío
+message._send_message(with_commit=True)
+
+# 3. Verificar formato de número
+print(f"Número original: {message.mobile_number}")
+formatted = message._whatsapp_phone_format()
+print(f"Número formateado: {formatted}")
+```
+
+### Error: "Invalid phone number format"
+
+**Síntomas:**
+- Números no se formatean correctamente
+- Error en validación de teléfonos
+
+**Diagnóstico:**
+```python
+# Verificar formateo de números
+message = env['whatsapp.message'].browse(message_id)
+original = message.mobile_number
+formatted = message._whatsapp_phone_format()
+print(f"Original: {original}")
+print(f"Formateado: {formatted}")
+```
+
+**Soluciones:**
+```python
+# Corregir formato manualmente
+def format_phone_number(number):
+    # Limpiar número
+    clean = number.replace(' ', '').replace('+', '').replace('-', '')
+    
+    # Aplicar reglas de formateo
+    if clean.startswith("52") and len(clean) == 12:
+        clean = "521" + clean[2:]
+    elif len(clean) == 10:
+        clean = "521" + clean
+    
+    return clean + '@c.us'
+
+# Aplicar corrección
+message.write({
+    'mobile_number': format_phone_number(message.mobile_number)
+})
+```
+
+### Error: "Attachment upload failed"
+
+**Síntomas:**
+- Adjuntos no se envían
+- Error al procesar archivos
+
+**Diagnóstico:**
+```python
+# Verificar adjunto
+attachment = env['ir.attachment'].browse(attachment_id)
+print(f"Nombre: {attachment.name}")
+print(f"Tipo: {attachment.mimetype}")
+print(f"Tamaño: {attachment.file_size}")
+print(f"Datos: {len(attachment.raw) if attachment.raw else 'No hay datos'}")
+```
+
+**Soluciones:**
+```python
+# 1. Verificar tamaño de archivo (límite WhatsApp: 100MB)
+if attachment.file_size > 100 * 1024 * 1024:
+    print("Archivo demasiado grande")
+
+# 2. Verificar tipo MIME
+valid_types = ['image/', 'video/', 'audio/', 'application/pdf', 'text/']
+if not any(attachment.mimetype.startswith(t) for t in valid_types):
+    print(f"Tipo de archivo no soportado: {attachment.mimetype}")
+
+# 3. Regenerar datos del archivo
+attachment.write({'raw': base64.b64encode(file_content)})
+```
+
+## Problemas de Grupos
+
+### Error: "Group not found"
+
+**Síntomas:**
+- Grupos no aparecen en la lista
+- Error al seleccionar grupo
+
+**Diagnóstico:**
+```python
+# Verificar grupos disponibles
+groups = env['ww.group'].search([])
+print(f"Grupos en base de datos: {len(groups)}")
+
+# Verificar sincronización
+account = env['whatsapp.account'].search([('whatsapp_web_url', '!=', False)], limit=1)
+api_groups = account.get_groups()
+print(f"Grupos desde API: {len(api_groups)}")
+```
+
+**Soluciones:**
+```python
+# 1. Sincronizar grupos manualmente
+env['ww.group'].sync_ww_contacts_groups()
+
+# 2. Verificar respuesta de API
+for group_data in api_groups:
+    print(f"Grupo API: {group_data.get('name')} - {group_data.get('id', {}).get('_serialized')}")
+
+# 3. Crear grupo manualmente si es necesario
+group = env['ww.group'].create({
+    'name': 'Mi Grupo',
+    'whatsapp_web_id': '120363158956331133@g.us',
+    'whatsapp_account_id': account.id
+})
+```
+
+### Error: "No members in group"
+
+**Síntomas:**
+- Grupos sin contactos
+- Canales no se crean
+
+**Diagnóstico:**
+```python
+# Verificar miembros de grupo
+group = env['ww.group'].browse(group_id)
+print(f"Contactos en grupo: {len(group.contact_ids)}")
+
+# Verificar datos de API
+account = group.whatsapp_account_id
+api_groups = account.get_groups()
+for api_group in api_groups:
+    if api_group['id']['_serialized'] == group.whatsapp_web_id:
+        members = api_group.get('members', [])
+        print(f"Miembros en API: {len(members)}")
+        break
+```
+
+**Soluciones:**
+```python
+# 1. Sincronizar contactos del grupo
+group.write({'contact_ids': [(6, 0, contact_ids)]})
+
+# 2. Crear contactos si no existen
+for member in members:
+    contact = env['res.partner'].search([
+        ('whatsapp_web_id', '=', member['id']['_serialized'])
+    ])
+    
+    if not contact:
+        contact = env['res.partner'].create({
+            'name': member.get('name', 'Sin nombre'),
+            'mobile': member.get('number', ''),
+            'whatsapp_web_id': member['id']['_serialized']
+        })
+    
+    contact_ids.append(contact.id)
+```
+
+## Problemas de Sincronización
+
+### Error: "Sync failed" o timeout
+
+**Síntomas:**
+- Sincronización se interrumpe
+- Timeouts en operaciones largas
+
+**Diagnóstico:**
+```python
+# Verificar logs de sincronización
+import logging
+_logger = logging.getLogger(__name__)
+
+# Ejecutar sincronización con logging detallado
+try:
+    env['ww.group'].sync_ww_contacts_groups()
+except Exception as e:
+    _logger.error(f"Error en sincronización: {e}")
+```
+
+**Soluciones:**
+```python
+# 1. Sincronizar en lotes pequeños
+def sync_in_batches(batch_size=5):
+    accounts = env['whatsapp.account'].search([('whatsapp_web_url', '!=', False)])
+    
+    for account in accounts:
+        groups_data = account.get_groups()
+        
+        for i in range(0, len(groups_data), batch_size):
+            batch = groups_data[i:i + batch_size]
+            process_batch(batch, account)
+            env.cr.commit()  # Commit después de cada lote
+
+# 2. Aumentar timeout de requests
+import requests
+response = requests.post(url, json=payload, timeout=60)
+```
+
+### Error: "Duplicate contacts"
+
+**Síntomas:**
+- Contactos duplicados en grupos
+- Constraint violations
+
+**Diagnóstico:**
+```python
+# Buscar contactos duplicados
+contacts = env['res.partner'].search([
+    ('whatsapp_web_id', '!=', False)
+])
+
+# Verificar duplicados por número
+mobile_counts = {}
+for contact in contacts:
+    if contact.mobile:
+        mobile_counts[contact.mobile] = mobile_counts.get(contact.mobile, 0) + 1
+
+duplicates = {k: v for k, v in mobile_counts.items() if v > 1}
+print(f"Duplicados por móvil: {duplicates}")
+```
+
+**Soluciones:**
+```python
+# 1. Fusionar contactos duplicados
+def merge_duplicate_contacts():
+    contacts = env['res.partner'].search([('whatsapp_web_id', '!=', False)])
+    
+    for contact in contacts:
+        if contact.mobile and len(contact.mobile) >= 10:
+            last_10 = contact.mobile[-10:]
+            duplicates = env['res.partner'].search([
+                ('mobile', 'like', '%' + last_10),
+                ('id', '!=', contact.id)
+            ])
+            
+            if duplicates:
+                # Fusionar en el primer contacto
+                for dup in duplicates:
+                    dup.unlink()
+
+# 2. Limpiar relaciones duplicadas
+def cleanup_duplicate_relations():
+    relations = env['ww.group_contact_rel'].search([])
+    
+    for rel in relations:
+        duplicates = env['ww.group_contact_rel'].search([
+            ('group_id', '=', rel.group_id.id),
+            ('contact_id', '=', rel.contact_id.id),
+            ('id', '!=', rel.id)
+        ])
+        
+        if duplicates:
+            duplicates.unlink()
+```
+
+## Problemas de Rendimiento
+
+### Error: "Memory limit exceeded"
+
+**Síntomas:**
+- Procesos se terminan por falta de memoria
+- Sincronización falla con muchos datos
+
+**Soluciones:**
+```python
+# 1. Procesar en chunks más pequeños
+def process_large_dataset(data, chunk_size=100):
+    for i in range(0, len(data), chunk_size):
+        chunk = data[i:i + chunk_size]
+        process_chunk(chunk)
+        env.cr.commit()  # Commit frecuente
+        gc.collect()  # Liberar memoria
+
+# 2. Usar iteradores en lugar de listas
+def process_contacts_efficiently():
+    contacts = env['res.partner'].search([('whatsapp_web_id', '!=', False)])
+    
+    for contact in contacts:
+        process_contact(contact)
+        if contact.id % 100 == 0:  # Commit cada 100 registros
+            env.cr.commit()
+```
+
+### Error: "Database timeout"
+
+**Síntomas:**
+- Operaciones de base de datos timeout
+- Transacciones muy largas
+
+**Soluciones:**
+```python
+# 1. Usar transacciones más cortas
+def sync_with_short_transactions():
+    accounts = env['whatsapp.account'].search([])
+    
+    for account in accounts:
+        with env.cr.savepoint():  # Transacción por cuenta
+            sync_account_groups(account)
+            env.cr.commit()
+
+# 2. Deshabilitar autocommit para operaciones masivas
+def bulk_operations():
+    env.cr.autocommit = False
+    try:
+        # Operaciones masivas
+        bulk_create_contacts()
+        bulk_update_groups()
+        env.cr.commit()
+    except Exception:
+        env.cr.rollback()
+        raise
+    finally:
+        env.cr.autocommit = True
+```
+
+## Logs y Debugging
+
+### Configurar Logging Detallado
+
+```python
+# Configurar logging en código
+import logging
+_logger = logging.getLogger(__name__)
+
+# Logging detallado para debugging
+_logger.setLevel(logging.DEBUG)
+
+def debug_sync_process():
+    _logger.info("Iniciando sincronización")
+    
+    try:
+        # Proceso de sincronización
+        result = env['ww.group'].sync_ww_contacts_groups()
+        _logger.info(f"Sincronización completada: {result}")
+        
+    except Exception as e:
+        _logger.error(f"Error en sincronización: {e}", exc_info=True)
+```
+
+### Verificar Logs del Sistema
+
+```bash
+# Ver logs de Odoo en tiempo real
+tail -f /var/odoo/stg2.mcteam.run/logs/odoo-server.log | grep -i "whatsapp"
+
+# Filtrar por módulo específico
+tail -f /var/odoo/stg2.mcteam.run/logs/odoo-server.log | grep -i "whatsapp_web"
+
+# Buscar errores específicos
+grep -i "error\|exception\|traceback" /var/odoo/stg2.mcteam.run/logs/odoo-server.log | grep -i "whatsapp"
+```
+
+### Debug desde Shell de Odoo
+
+```bash
+# Acceder al shell de Odoo
+cd /var/odoo/mcteam.run
+sudo -u odoo venv/bin/python3 src/odoo-bin shell -c odoo.conf
+```
+
+```python
+# En el shell de Odoo
+# Verificar estado de módulos
+modules = env['ir.module.module'].search([('name', 'ilike', 'whatsapp')])
+for module in modules:
+    print(f"{module.name}: {module.state}")
+
+# Verificar configuración
+accounts = env['whatsapp.account'].search([])
+for account in accounts:
+    print(f"Cuenta: {account.name}")
+    print(f"URL: {account.whatsapp_web_url}")
+    print(f"Activa: {account.active}")
+
+# Probar conectividad
+import requests
+account = accounts[0]
+try:
+    response = requests.post(account.whatsapp_web_url, 
+                           json={"method": "ping", "args": []}, 
+                           timeout=10)
+    print(f"Respuesta: {response.status_code}")
+except Exception as e:
+    print(f"Error: {e}")
+```
+
+### Debug de Base de Datos
+
+```bash
+# Verificar registros en base de datos
+su - postgres -c "psql stg2.mcteam.run -c \"SELECT COUNT(*) FROM ww_group;\" | cat"
+
+# Verificar configuración de cuentas
+su - postgres -c "psql stg2.mcteam.run -c \"SELECT name, whatsapp_web_url FROM whatsapp_account;\" | cat"
+
+# Verificar mensajes pendientes
+su - postgres -c "psql stg2.mcteam.run -c \"SELECT COUNT(*) FROM whatsapp_message WHERE state = 'outgoing';\" | cat"
+```
+
+## Herramientas de Diagnóstico
+
+### Script de Diagnóstico Completo
+
+```python
+def diagnose_whatsapp_web():
+    """Script completo de diagnóstico"""
+    
+    print("=== DIAGNÓSTICO WHATSAPP WEB ===")
+    
+    # 1. Verificar módulos
+    print("\n1. Verificando módulos...")
+    modules = env['ir.module.module'].search([('name', 'ilike', 'whatsapp')])
+    for module in modules:
+        print(f"  {module.name}: {module.state}")
+    
+    # 2. Verificar cuentas
+    print("\n2. Verificando cuentas...")
+    accounts = env['whatsapp.account'].search([])
+    for account in accounts:
+        print(f"  {account.name}: {account.whatsapp_web_url or 'Sin URL'}")
+    
+    # 3. Verificar grupos
+    print("\n3. Verificando grupos...")
+    groups = env['ww.group'].search([])
+    print(f"  Total grupos: {len(groups)}")
+    for group in groups:
+        print(f"    {group.name}: {len(group.contact_ids)} contactos")
+    
+    # 4. Verificar mensajes
+    print("\n4. Verificando mensajes...")
+    messages = env['whatsapp.message'].search([])
+    print(f"  Total mensajes: {len(messages)}")
+    
+    outgoing = messages.filtered(lambda m: m.state == 'outgoing')
+    print(f"  Pendientes: {len(outgoing)}")
+    
+    # 5. Probar conectividad
+    print("\n5. Probando conectividad...")
+    web_account = accounts.filtered(lambda a: a.whatsapp_web_url)
+    if web_account:
+        try:
+            import requests
+            response = requests.post(web_account[0].whatsapp_web_url, 
+                                   json={"method": "ping", "args": []}, 
+                                   timeout=10)
+            print(f"  Conectividad: OK ({response.status_code})")
+        except Exception as e:
+            print(f"  Conectividad: ERROR - {e}")
+    else:
+        print("  Conectividad: No hay cuentas WhatsApp Web configuradas")
+
+# Ejecutar diagnóstico
+diagnose_whatsapp_web()
+```
+
+Este script proporciona una visión completa del estado del sistema y ayuda a identificar problemas comunes.
+

+ 4 - 2
__manifest__.py

@@ -9,9 +9,11 @@
     """,
     'author': 'Tu Nombre',
     'website': 'https://www.tuempresa.com',
-    'depends': ['whatsapp'],
+    'depends': ['whatsapp', 'marketing_automation_whatsapp'],
     'data': [
-        'views/whatsapp_account_views.xml'
+        'views/whatsapp_account_views.xml',
+        'views/whatsapp_message_views.xml',
+        'views/whatsapp_composer_views.xml'
     ],
     'installable': True,
     'auto_install': False,

TEMPAT SAMPAH
__pycache__/__init__.cpython-310.pyc


+ 3 - 1
models/__init__.py

@@ -1,3 +1,5 @@
+from . import whatsapp_account
 from . import whatsapp_message
+from . import whatsapp_composer
 from . import whatsapp_patch
-from . import whatsapp_account
+from . import mail_message

TEMPAT SAMPAH
models/__pycache__/__init__.cpython-310.pyc


TEMPAT SAMPAH
models/__pycache__/marketing_activity.cpython-310.pyc


TEMPAT SAMPAH
models/__pycache__/res_config_settings.cpython-310.pyc


TEMPAT SAMPAH
models/__pycache__/whatsapp_message.cpython-310.pyc


+ 130 - 0
models/mail_message.py

@@ -0,0 +1,130 @@
+import logging
+import requests
+from odoo import models
+from odoo.addons.mail.tools.discuss import Store
+from odoo.addons.whatsapp.tools.whatsapp_exception import WhatsAppError
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+class MailMessage(models.Model):
+    _inherit = 'mail.message'
+
+    def _message_reaction(self, content, action, partner, guest, store: Store = None):
+        """Sobrescribir para usar WhatsApp Web API Gateway cuando esté configurado"""
+        # Si es mensaje de WhatsApp, verificar si usa WhatsApp Web
+        if self.message_type == "whatsapp_message" and self.wa_message_ids:
+            wa_msg = self.wa_message_ids[0]
+            
+            # Verificar si la cuenta usa WhatsApp Web
+            if wa_msg.wa_account_id and wa_msg.wa_account_id.whatsapp_web_url:
+                # Usar API Gateway para WhatsApp Web
+                self._send_whatsapp_web_reaction(wa_msg, content, action, partner, guest, store)
+                # Actualizar UI directamente usando el método base de mail (sin pasar por enterprise)
+                # Esto evita que el método del enterprise intente enviar de nuevo
+                return self._update_reaction_ui(content, action, partner, guest, store)
+            else:
+                # Usar método original para WhatsApp Business API (enterprise)
+                # Este llamará a super() al final para actualizar la UI
+                return super()._message_reaction(content, action, partner, guest, store)
+        
+        # Para mensajes que no son de WhatsApp, usar método base
+        return super()._message_reaction(content, action, partner, guest, store)
+    
+    def _update_reaction_ui(self, content, action, partner, guest, store: Store = None):
+        """Actualizar la UI de reacciones sin intentar enviar (para WhatsApp Web)"""
+        self.ensure_one()
+        # Buscar reacción existente
+        domain = [
+            ("message_id", "=", self.id),
+            ("partner_id", "=", partner.id),
+            ("guest_id", "=", guest.id),
+            ("content", "=", content),
+        ]
+        reaction = self.env["mail.message.reaction"].search(domain)
+        # Crear/eliminar reacción según la acción
+        if action == "add" and not reaction:
+            create_values = {
+                "message_id": self.id,
+                "content": content,
+                "partner_id": partner.id,
+                "guest_id": guest.id,
+            }
+            self.env["mail.message.reaction"].create(create_values)
+        if action == "remove" and reaction:
+            reaction.unlink()
+        if store:
+            # Llenar el store para usuarios portal no autenticados
+            self._reaction_group_to_store(store, content)
+        # Enviar el grupo de reacciones al bus para usuarios autenticados
+        self._bus_send_reaction_group(content)
+
+    def _send_whatsapp_web_reaction(self, wa_msg, content, action, partner, guest, store: Store = None):
+        """Enviar reacción usando WhatsApp Web API Gateway"""
+        self.ensure_one()
+        
+        account = wa_msg.wa_account_id
+        url = account.whatsapp_web_url
+        session_name = account.whatsapp_web_login
+        api_key = account.whatsapp_web_api_key
+        
+        if not all([url, session_name, api_key]):
+            raise UserError("WhatsApp Web no está completamente configurado. Faltan URL, Login o API Key.")
+        
+        # Manejar reacciones previas (igual que el método original)
+        if action == "add":
+            previous_reaction = self.env["mail.message.reaction"].search([
+                ("message_id", "=", self.id),
+                ("partner_id", "=", partner.id),
+                ("guest_id", "=", guest.id),
+            ], limit=1)
+            if previous_reaction:
+                previous_reaction_emoji = previous_reaction.content
+                if previous_reaction_emoji == content:
+                    return
+                previous_reaction.unlink()
+                self._bus_send_reaction_group(previous_reaction_emoji)
+        
+        # Obtener el ID del mensaje original
+        message_id = wa_msg.msg_uid
+        if not message_id:
+            raise UserError("No se puede enviar reacción: el mensaje no tiene ID válido.")
+        
+        # Construir URL y payload para la API Gateway
+        base_url = url.rstrip('/')
+        endpoint = 'send-reaction'
+        full_url = f"{base_url}/api/v1/{session_name}/{endpoint}"
+        
+        # Determinar emoji (vacío si es remover)
+        emoji = content if action == "add" else ""
+        
+        payload = {
+            "messageId": message_id,
+            "emoji": emoji
+        }
+        
+        headers = {
+            "Content-Type": "application/json",
+            "X-API-Key": api_key
+        }
+        
+        try:
+            _logger.info("Enviando reacción %s al mensaje %s", emoji or "vacía", message_id)
+            response = requests.post(full_url, json=payload, headers=headers, timeout=30)
+            
+            if response.status_code == 200:
+                _logger.info("Reacción enviada exitosamente a WhatsApp Web")
+                # No retornar aquí, dejar que el método padre actualice la UI
+                return
+            else:
+                error_text = response.text
+                _logger.error("Error al enviar reacción. Código: %s, Respuesta: %s", response.status_code, error_text)
+                raise UserError(f"Error al enviar reacción: {error_text}")
+                
+        except requests.exceptions.RequestException as e:
+            _logger.error("Error de conexión al enviar reacción: %s", str(e))
+            raise UserError(f"Error de conexión al enviar reacción: {str(e)}")
+        except Exception as e:
+            _logger.error("Error inesperado al enviar reacción: %s", str(e))
+            raise UserError(f"Error al enviar reacción: {str(e)}")
+

+ 51 - 2
models/whatsapp_account.py

@@ -1,10 +1,59 @@
 import logging
+import requests
+import json
 
 from odoo import fields, models
 
 _logger = logging.getLogger(__name__)
 
 class WhatsAppAccount(models.Model):
-    _inherit = ['whatsapp.account']
+    _inherit = 'whatsapp.account'
 
-    whatsapp_web_url = fields.Char(string="WhatsApp Web URL", readonly=False, copy=False)
+    whatsapp_web_url = fields.Char(string="WhatsApp Web URL", readonly=False, copy=False)
+    whatsapp_web_login = fields.Char(string="Login", readonly=False, copy=False)
+    whatsapp_web_api_key = fields.Char(string="API Key", readonly=False, copy=False)
+
+    def get_groups(self):
+        """
+        Obtiene los grupos de WhatsApp Web para la cuenta desde la base de datos de la plataforma.
+        Returns:
+            list: Lista de diccionarios con la información de los grupos en formato compatible con Odoo
+        """
+        self.ensure_one()
+        
+        if not self.whatsapp_web_url:
+            _logger.warning("No se ha configurado la URL de WhatsApp Web para la cuenta %s", self.name)
+            return []
+
+        if not self.whatsapp_web_login:
+            _logger.warning("No se ha configurado el Login (session_name) para la cuenta %s", self.name)
+            return []
+
+        if not self.whatsapp_web_api_key:
+            _logger.warning("No se ha configurado la API Key para la cuenta %s", self.name)
+            return []
+
+        try:
+            # Construir URL del nuevo endpoint
+            base_url = self.whatsapp_web_url.rstrip('/')
+            session_name = self.whatsapp_web_login
+            url = f"{base_url}/api/v1/{session_name}/groups"
+            
+            headers = {
+                "Content-Type": "application/json",
+                "X-API-Key": self.whatsapp_web_api_key
+            }
+            
+            response = requests.get(url, headers=headers, timeout=30)
+            
+            if response.status_code == 200:
+                groups = response.json()
+                _logger.info("Grupos obtenidos desde la base de datos: %d grupos", len(groups))
+                return groups
+            else:
+                _logger.error("Error al obtener groups: %s - %s", response.status_code, response.text)
+                return []
+                
+        except Exception as e:
+            _logger.error("Error en la petición de groups: %s", str(e))
+            return []

+ 217 - 0
models/whatsapp_composer.py

@@ -0,0 +1,217 @@
+from odoo import models, fields, api
+from odoo.exceptions import ValidationError
+import logging
+
+_logger = logging.getLogger(__name__)
+
+class WhatsAppComposer(models.TransientModel):
+    _inherit = 'whatsapp.composer'
+
+    # Campos para soporte básico de grupos (solo por ID string, sin Many2one)
+    # La funcionalidad completa de grupos con Many2one está en whatsapp_web_groups
+    recipient_type = fields.Selection([
+        ('phone', 'Phone Number'),
+        ('group', 'WhatsApp Group')
+    ], string='Send To', default='phone', help="Choose recipient type")
+    
+    # Campo para ID de grupo como string
+    whatsapp_group_id_char = fields.Char(string='Group ID', 
+                                         help="WhatsApp Group ID (e.g., 120363158956331133@g.us)")
+    
+    # Campo para mensaje libre (sin plantilla)
+    body = fields.Html(string='Message Body', help="Free text message (for WhatsApp Web accounts without template)")
+    
+    @api.constrains('recipient_type', 'phone', 'whatsapp_group_id_char', 'wa_template_id', 'body')
+    def _check_recipient_configuration(self):
+        """Validar configuración de destinatario en composer"""
+        for record in self:
+            # Si está en contexto de skip_template_validation, saltar validaciones de plantilla
+            if self.env.context.get('skip_template_validation'):
+                # Solo validar configuración básica de destinatario
+                if record.recipient_type == 'group' and not record.whatsapp_group_id_char:
+                    raise ValidationError("Please enter a Group ID when sending to groups")
+                elif record.recipient_type == 'phone' and not record.phone:
+                    raise ValidationError("Please provide a phone number when sending to individuals")
+                return  # Saltar el resto de validaciones
+            
+            # Detectar si hay cuentas de WhatsApp Web disponibles
+            whatsapp_web_accounts = record.env['whatsapp.account'].search([
+                ('whatsapp_web_url', '!=', False)
+            ])
+            has_whatsapp_web = bool(whatsapp_web_accounts)
+            
+            if record.recipient_type == 'group' and not record.whatsapp_group_id_char:
+                raise ValidationError("Please enter a Group ID when sending to groups")
+            elif record.recipient_type == 'phone' and not record.phone:
+                raise ValidationError("Please provide a phone number when sending to individuals")
+            
+            # Validar que haya contenido (plantilla o mensaje libre)
+            if not record.wa_template_id and not record.body:
+                if has_whatsapp_web:
+                    raise ValidationError("Please provide either a template or write a free text message")
+                else:
+                    raise ValidationError("Template is required for WhatsApp Business API")
+            
+            # Si usa mensaje libre, debe haber WhatsApp Web
+            if record.body and not record.wa_template_id and not has_whatsapp_web:
+                raise ValidationError("Free text messages require WhatsApp Web account configuration")
+    
+    @api.depends('phone', 'batch_mode', 'recipient_type', 'whatsapp_group_id_char')
+    def _compute_invalid_phone_number_count(self):
+        """Override SOLO para casos específicos de grupos - NO interferir con funcionalidad nativa"""
+        for composer in self:
+            # SOLO intervenir si es un caso muy específico de grupo
+            if (hasattr(composer, 'recipient_type') and composer.recipient_type == 'group'):
+                composer.invalid_phone_number_count = 0
+                continue
+                
+            # SOLO intervenir si el phone es explícitamente un ID de grupo
+            if composer.phone and composer.phone.endswith('@g.us'):
+                composer.invalid_phone_number_count = 0
+                continue
+                
+            # TODOS LOS DEMÁS CASOS: usar lógica original sin modificar
+            super(WhatsAppComposer, composer)._compute_invalid_phone_number_count()
+    
+    def action_send_whatsapp_template(self):
+        """Override del método de envío SOLO para casos específicos de WhatsApp Web sin plantilla"""
+        
+        # SOLO intervenir si es un caso muy específico:
+        # 1. No hay plantilla
+        # 2. Hay mensaje libre (body)
+        # 3. Es tipo grupo O hay WhatsApp Web disponible
+        whatsapp_web_accounts = self.env['whatsapp.account'].search([
+            ('whatsapp_web_url', '!=', False)
+        ])
+        has_whatsapp_web = bool(whatsapp_web_accounts)
+        
+        # CONDICIÓN MUY ESPECÍFICA para no interferir con funcionalidad nativa
+        is_special_case = (
+            not self.wa_template_id and  # Sin plantilla
+            self.body and  # Con mensaje libre
+            has_whatsapp_web and  # Con WhatsApp Web disponible
+            (self.recipient_type == 'group' or  # Es grupo
+             (hasattr(self, 'phone') and self.phone and self.phone.endswith('@g.us')))  # O es ID de grupo directo
+        )
+        
+        if is_special_case:
+            return self._send_whatsapp_web_message()
+        
+        # TODOS LOS DEMÁS CASOS: usar método original sin modificar
+        return super().action_send_whatsapp_template()
+    
+    def _send_whatsapp_web_message(self):
+        """Enviar mensaje WhatsApp Web sin plantilla - siguiendo lógica original"""
+        records = self._get_active_records()
+        
+        for record in records:
+            # Determinar destinatario
+            if self.recipient_type == 'group':
+                if self.whatsapp_group_id_char:
+                    mobile_number = self.whatsapp_group_id_char
+                else:
+                    raise ValidationError("Please specify a group ID")
+            else:
+                mobile_number = self.phone
+                if not mobile_number:
+                    raise ValidationError("Please provide a phone number")
+            
+            # Crear mail.message con adjuntos si existen (siguiendo lógica original)
+            post_values = {
+                'attachment_ids': [self.attachment_id.id] if self.attachment_id else [],
+                'body': self.body,
+                'message_type': 'whatsapp_message',
+                'partner_ids': hasattr(record, '_mail_get_partners') and record._mail_get_partners()[record.id].ids or record._whatsapp_get_responsible().partner_id.ids,
+            }
+            
+            if hasattr(records, '_message_log'):
+                message = record._message_log(**post_values)
+            else:
+                message = self.env['mail.message'].create(
+                    dict(post_values, res_id=record.id, model=self.res_model,
+                         subtype_id=self.env['ir.model.data']._xmlid_to_res_id("mail.mt_note"))
+                )
+            
+            # Crear mensaje WhatsApp (siguiendo estructura original)
+            whatsapp_message = self.env['whatsapp.message'].create({
+                'mail_message_id': message.id,
+                'mobile_number': mobile_number,
+                'mobile_number_formatted': mobile_number,
+                'recipient_type': self.recipient_type,
+                'wa_template_id': False,  # Sin plantilla
+                'wa_account_id': self._get_whatsapp_web_account().id,
+                'state': 'outgoing',
+            })
+            
+            # Enviar mensaje usando la lógica original de _send_message
+            whatsapp_message._send_message()
+        
+        return {'type': 'ir.actions.act_window_close'}
+    
+    def _prepare_whatsapp_message_values(self, record):
+        """Override SOLO para agregar información de grupo - NO interferir con funcionalidad nativa"""
+        
+        # SIEMPRE usar lógica original primero
+        values = super()._prepare_whatsapp_message_values(record)
+        
+        # SOLO agregar información de grupo si es caso específico
+        if (hasattr(self, 'recipient_type') and self.recipient_type == 'group'):
+            if self.whatsapp_group_id_char:
+                values.update({
+                    'recipient_type': 'group',
+                    'mobile_number': self.whatsapp_group_id_char,
+                    'mobile_number_formatted': self.whatsapp_group_id_char,
+                })
+        
+        # Siempre agregar recipient_type para compatibilidad
+        if not values.get('recipient_type'):
+            values['recipient_type'] = 'phone'
+        
+        return values
+    
+    def _get_whatsapp_web_account(self):
+        """Obtener cuenta de WhatsApp Web disponible"""
+        # Primero intentar usar la cuenta de la plantilla si existe
+        if self.wa_template_id and self.wa_template_id.wa_account_id and self.wa_template_id.wa_account_id.whatsapp_web_url:
+            return self.wa_template_id.wa_account_id
+            
+        # Si no, buscar cualquier cuenta con WhatsApp Web
+        account = self.env['whatsapp.account'].search([
+            ('whatsapp_web_url', '!=', False)
+        ], limit=1)
+        
+        if not account:
+            raise ValidationError("No WhatsApp Web account configured")
+            
+        return account
+    
+    def _send_whatsapp_message_without_template(self, body, phone=None, group_id=None):
+        """Enviar mensaje de WhatsApp sin plantilla (solo para WhatsApp Web)"""
+        # Solo funciona con WhatsApp Web, no con API oficial
+        if not (phone or group_id):
+            raise ValidationError("Debe especificar teléfono o grupo")
+            
+        # Crear mensaje directamente
+        message_vals = {
+            'body': body,
+            'mobile_number': group_id or phone,
+            'recipient_type': 'group' if group_id else 'phone',
+            'wa_template_id': False,  # Sin plantilla
+            'state': 'outgoing',
+        }
+        
+        # Nota: El campo whatsapp_group_id Many2one está en whatsapp_web_groups
+            
+        # Crear mail.message
+        mail_message = self.env['mail.message'].create({
+            'body': body,
+            'message_type': 'whatsapp_message',
+        })
+        
+        message_vals['mail_message_id'] = mail_message.id
+        
+        # Crear y enviar mensaje WhatsApp
+        whatsapp_message = self.env['whatsapp.message'].create(message_vals)
+        whatsapp_message._send_message()
+        
+        return whatsapp_message

+ 244 - 57
models/whatsapp_message.py

@@ -1,5 +1,6 @@
 from odoo import models, fields, api
 from odoo.tools import groupby
+from odoo.exceptions import ValidationError
 import logging
 import markupsafe
 import requests
@@ -15,38 +16,104 @@ _logger = logging.getLogger(__name__)
 class WhatsAppMessage(models.Model):
     _inherit = 'whatsapp.message'
 
+    # Campos para soporte básico de grupos (solo por ID string, sin Many2one)
+    # La funcionalidad completa de grupos con Many2one está en whatsapp_web_groups
+    recipient_type = fields.Selection([
+        ('phone', 'Phone Number'),
+        ('group', 'WhatsApp Group')
+    ], string='Recipient Type', default='phone', help="Type of recipient: phone number or WhatsApp group")
+    
+    @api.depends('recipient_type', 'mobile_number')
+    def _compute_final_recipient(self):
+        """Compute the final recipient based on type"""
+        for record in self:
+            # Si es grupo y mobile_number termina en @g.us, usarlo directamente
+            if record.recipient_type == 'group' and record.mobile_number and record.mobile_number.endswith('@g.us'):
+                record.final_recipient = record.mobile_number
+            else:
+                record.final_recipient = record.mobile_number
+    
+    final_recipient = fields.Char('Final Recipient', compute='_compute_final_recipient', 
+                                  help="Final recipient (phone or group ID)")
+    
+    @api.depends('mobile_number', 'recipient_type')
+    def _compute_mobile_number_formatted(self):
+        """Override SOLO para casos específicos de grupos con WhatsApp Web"""
+        for message in self:
+            # SOLO intervenir si es grupo CON WhatsApp Web configurado
+            if (hasattr(message, 'recipient_type') and message.recipient_type == 'group' and
+                message.wa_account_id and message.wa_account_id.whatsapp_web_url and
+                message.mobile_number and message.mobile_number.endswith('@g.us')):
+                
+                message.mobile_number_formatted = message.mobile_number
+            else:
+                # TODOS LOS DEMÁS CASOS: usar lógica original sin modificar
+                super(WhatsAppMessage, message)._compute_mobile_number_formatted()
+    
+    @api.constrains('recipient_type', 'mobile_number')
+    def _check_recipient_configuration(self):
+        """Validar configuración de destinatario"""
+        for record in self:
+            if record.recipient_type == 'group':
+                if not (record.mobile_number and record.mobile_number.endswith('@g.us')):
+                    raise ValidationError("Para mensajes a grupos, debe proporcionar un ID de grupo válido (@g.us)")
+            elif record.recipient_type == 'phone':
+                if not record.mobile_number or record.mobile_number.endswith('@g.us'):
+                    raise ValidationError("Para mensajes a teléfonos, debe proporcionar un número telefónico válido")
+    
+    def _whatsapp_phone_format(self, fpath=None, number=None, raise_on_format_error=False):
+        """Override SOLO para casos específicos de grupos - NO interferir con funcionalidad nativa"""
+        self.ensure_one()
+        
+        # SOLO intervenir en casos muy específicos de grupos
+        # Si es un mensaje a grupo Y tiene WhatsApp Web configurado
+        if (hasattr(self, 'recipient_type') and self.recipient_type == 'group' and 
+            self.wa_account_id and self.wa_account_id.whatsapp_web_url):
+            
+            # Si el número es un ID de grupo (termina en @g.us), retornarlo sin validar
+            if number and number.endswith('@g.us'):
+                return number
+            elif self.mobile_number and self.mobile_number.endswith('@g.us'):
+                return self.mobile_number
+            
+        # TODOS LOS DEMÁS CASOS: usar validación original sin modificar
+        return super()._whatsapp_phone_format(fpath, number, raise_on_format_error)
+    
+    def _get_final_destination(self):
+        """Método mejorado para obtener destino final (grupo o teléfono)"""
+        self.ensure_one()
+        
+        # Si el mobile_number es un ID de grupo (termina en @g.us)
+        if self.mobile_number and self.mobile_number.endswith('@g.us'):
+            return self.mobile_number
+            
+        return False
     
     def _send_message(self, with_commit=False):
 
         url = ''
+        session_name = ''
+        api_key = ''
 
         if self.wa_account_id and self.wa_account_id.whatsapp_web_url:
             url = self.wa_account_id.whatsapp_web_url
-            _logger.info('WHATSAPP WEB SEND MESSAGE' + url)
+            session_name = self.wa_account_id.whatsapp_web_login or ''
+            api_key = self.wa_account_id.whatsapp_web_api_key or ''
+            _logger.info('WHATSAPP WEB SEND MESSAGE - URL: %s, Session: %s', url, session_name)
 
         group = ''
         
-        if not url:
+        if not url or not session_name or not api_key:
+            # Si no hay configuración de WhatsApp Web, usar método original
             super()._send_message(with_commit)
+            return
             
         for whatsapp_message in self:
-            #verificacion envio a grupo
-            #plantilla dada de alta en x_plantillas_whatsapp 
-            if 'marketing.trace' in self.env:
-                marketing_traces = self.env['marketing.trace'].sudo().search([('whatsapp_message_id', '=', whatsapp_message.id)])
-                for marketing_trace in marketing_traces:
-                    if 'x_studio_grupo_whatsapp' in marketing_trace.activity_id and marketing_trace.activity_id.x_studio_grupo_whatsapp:
-                        group = marketing_trace.activity_id.x_studio_grupo_whatsapp.x_studio_destinatario
-
-            if 'x_notificaciones_whats' in self.env and not group: 
-                notificaciones = self.env['x_notificaciones_whats'].sudo().search([('x_studio_plantilla_de_whatsapp', '=', whatsapp_message.wa_template_id.id)])
-                if notificaciones:
-                    _logger.info('template encontrado')
-                    if not group: 
-                        for notificacion in notificaciones:
-                            if not notificacion.x_studio_partner_unico:
-                                group = notificacion.x_studio_destinatario
-                                break
+            # Determinar destinatario final usando solo la nueva lógica
+            final_destination = whatsapp_message._get_final_destination()
+            
+            if final_destination:
+                group = final_destination
             
             attachment = False
 
@@ -74,9 +141,14 @@ class WhatsAppMessage(models.Model):
 
             #codigo para limpiar body y numero
             body = whatsapp_message.body
-            if isinstance(body, markupsafe.Markup):
-                text = html.unescape(str(body))
             
+            # Asegurar que body sea string y limpiar HTML
+            if body:
+                if isinstance(body, markupsafe.Markup):
+                    text = html.unescape(str(body))
+                else:
+                    text = str(body)
+                
                 # Reemplazamos las etiquetas BR y P
                 text = re.sub(r'<br\s*/?>|<BR\s*/?>', '\n', text)
                 text = re.sub(r'<p>|<P>', '\n\n', text)
@@ -90,57 +162,172 @@ class WhatsAppMessage(models.Model):
                 
                 # Limpiamos espacios en blanco al inicio y final
                 body = text.strip()
+                
+                # Asegurar que no esté vacío
+                if not body:
+                    body = "Mensaje de WhatsApp"
+            else:
+                body = "Mensaje de WhatsApp"
 
-            number = whatsapp_message.mobile_number
-            number = number.replace(' ', '').replace('+','')
-
-            if number.startswith("52") and len(number) == 12:
-                number = "521" + number[2:]
+            # Determinar número/destinatario final
+            if group:
+                # Si ya hay un grupo determinado, usarlo
+                number = group
+            else:
+                # Formatear número según el tipo de destinatario
+                if whatsapp_message.recipient_type == 'group':
+                    if whatsapp_message.mobile_number and whatsapp_message.mobile_number.endswith('@g.us'):
+                        number = whatsapp_message.mobile_number
+                    else:
+                        _logger.error("Mensaje configurado como grupo pero sin destinatario válido")
+                        continue
+                else:
+                    # Lógica original para números de teléfono
+                    number = whatsapp_message.mobile_number
+                    if number:
+                        number = number.replace(' ', '').replace('+','').replace('-','')
 
-            # ENVIO DE MENSAJE
-            # Headers de la petición, si es necesario
-            headers = {
-                "Content-Type": "application/json"
-            }
+                        if number.startswith("52") and len(number) == 12:
+                            number = "521" + number[2:]
+                        
+                        if len(number) == 10:
+                            number = "521" + number
+                        
+                        number = number + '@c.us'
 
-            number = group if group else number + '@c.us'
-            
-            #$wa::sendMessage("521{$fields_data[$settings['borax_whatsapp_mobile']]}@c.us", ['type' => 'MessageMedia', 'args' => [mime_content_type($file), base64_encode(file_get_contents($file)), $filename, $filesize]], ['caption' => $borax_whatsapp_mensaje]);
+            # ENVIO DE MENSAJE - Nueva API Gateway
             parent_message_id = ''
             if whatsapp_message.mail_message_id and whatsapp_message.mail_message_id.parent_id:
                     parent_id = whatsapp_message.mail_message_id.parent_id.wa_message_ids
                     if parent_id:
                         parent_message_id = parent_id[0].msg_uid
+            
+            # Validar que tenemos un destinatario válido
+            if not number:
+                _logger.error("No se pudo determinar el destinatario para el mensaje")
+                continue
+                
+            # Validar que tenemos un cuerpo de mensaje válido
+            if not body or not isinstance(body, str):
+                _logger.error("Cuerpo del mensaje inválido: %s", body)
+                body = "Mensaje de WhatsApp"
+            
+            # Determinar si es grupo
+            is_group = number.endswith('@g.us') if number else False
+            
+            # Construir URL base
+            base_url = url.rstrip('/')
+            endpoint = 'send-message'
+            
+            # Headers con autenticación
+            headers = {
+                "Content-Type": "application/json",
+                "X-API-Key": api_key
+            }
+            
+            # Preparar payload según tipo de mensaje
             if attachment:
+                # Determinar endpoint según tipo de archivo
+                mimetype = attachment.mimetype or 'application/octet-stream'
+                if mimetype.startswith('image/'):
+                    endpoint = 'send-image'
+                elif mimetype.startswith('video/'):
+                    endpoint = 'send-video'
+                elif mimetype.startswith('audio/'):
+                    endpoint = 'send-voice'
+                else:
+                    endpoint = 'send-file'
+                
+                # Convertir archivo a base64 con prefijo data URI
+                file_base64 = base64.b64encode(attachment.raw).decode('utf-8')
+                base64_with_prefix = f"data:{mimetype};base64,{file_base64}"
+                
                 payload = {
-                    "method": "sendMessage",  
-                    "args": [number, {'type': 'MessageMedia', 'args': [attachment.mimetype, base64.b64encode(attachment.raw).decode('utf-8'), attachment.name, attachment.file_size]}, {'caption': body}]
+                    "phone": [number],  # Array para send-image/send-file
+                    "base64": base64_with_prefix,
+                    "filename": attachment.name or "file",
+                    "caption": body,
+                    "isGroup": is_group
                 }
-            else: 
+                
+                if parent_message_id:
+                    payload["quotedMessageId"] = parent_message_id
+            else:
+                # Mensaje de texto
                 payload = {
-                    "method": "sendMessage",  
-                    "args": [number, body, {}]
+                    "phone": number,  # String para send-message
+                    "message": body,
+                    "isGroup": is_group
                 }
-
-            if parent_message_id:
-                payload['args'][2]['quotedMessageId'] = parent_message_id
-
-            # Realizando la petición POST
-            response = requests.post(url, data=json.dumps(payload), headers=headers)
-
-            # Verificando si la respuesta contiene data->id
-            if response.status_code == 200:
-                response_json = response.json()
-                if "_data" in response_json and "id" in response_json["_data"]:
-                    _logger.info(f"Petición exitosa. ID: {response_json['_data']['id']['id']}")
+                
+                if parent_message_id:
+                    payload["quotedMessageId"] = parent_message_id
+            
+            # Construir URL completa
+            full_url = f"{base_url}/api/v1/{session_name}/{endpoint}"
+            
+            # Log del payload para debugging
+            _logger.info("Enviando mensaje a %s (%s) usando endpoint %s", number, "grupo" if is_group else "contacto", endpoint)
+            
+            # Realizar petición POST
+            try:
+                response = requests.post(full_url, json=payload, headers=headers, timeout=60)
+                
+                # Procesar respuesta
+                if response.status_code == 200:
+                    try:
+                        response_json = response.json()
+                        
+                        # La nueva API puede devolver jobId (mensaje encolado) o id (enviado directamente)
+                        if 'jobId' in response_json:
+                            # Mensaje encolado - si la API devuelve jobId, significa que el mensaje fue aceptado
+                            # y está en proceso de envío, por lo que lo marcamos como 'sent'
+                            job_id = response_json.get('jobId')
+                            _logger.info("Mensaje aceptado por la API. Job ID: %s - Marcando como enviado", job_id)
+                            whatsapp_message.write({
+                                'state': 'sent',  # Marcar como enviado ya que fue aceptado por la API
+                                'msg_uid': job_id
+                            })
+                            self._cr.commit()
+                        elif 'id' in response_json:
+                            # Mensaje enviado directamente
+                            msg_id = response_json.get('id')
+                            if isinstance(msg_id, dict) and '_serialized' in msg_id:
+                                msg_uid = msg_id['_serialized']
+                            elif isinstance(msg_id, str):
+                                msg_uid = msg_id
+                            else:
+                                msg_uid = str(msg_id)
+                            
+                            _logger.info("Mensaje enviado exitosamente. ID: %s", msg_uid)
+                            whatsapp_message.write({
+                                'state': 'sent',
+                                'msg_uid': msg_uid
+                            })
+                            self._cr.commit()
+                        else:
+                            _logger.warning("Respuesta exitosa pero sin jobId ni id: %s", response_json)
+                            whatsapp_message.write({
+                                'state': 'outgoing'
+                            })
+                            self._cr.commit()
+                    except ValueError:
+                        _logger.error("La respuesta no es JSON válido: %s", response.text)
+                        whatsapp_message.write({
+                            'state': 'error'
+                        })
+                        self._cr.commit()
+                else:
+                    _logger.error("Error en la petición. Código: %s, Respuesta: %s", response.status_code, response.text)
                     whatsapp_message.write({
-                        'state': 'sent',
-                        'msg_uid': response_json['_data']['id']['_serialized']
+                        'state': 'error'
                     })
                     self._cr.commit()
-                else:
-                    _logger.info("La respuesta no contiene 'data->id'.")
-            else:
-                _logger.info(f"Error en la petición. Código de estado: {response.status_code}")
+            except requests.exceptions.RequestException as e:
+                _logger.error("Error de conexión al enviar mensaje: %s", str(e))
+                whatsapp_message.write({
+                    'state': 'error'
+                })
+                self._cr.commit()
 
             time.sleep(random.randint(3, 7))

+ 26 - 0
models/whatsapp_patch.py

@@ -21,3 +21,29 @@ def custom_get_whatsapp_document(self, document_id):
 
 # Sobrescribir el método en tiempo de ejecución
 WhatsAppApi._get_whatsapp_document = custom_get_whatsapp_document
+
+# Parche para el método _post_whatsapp_reaction para evitar errores de constraint
+try:
+    from odoo.addons.whatsapp.models.mail_message import MailMessage
+    
+    # Guardar referencia al método original
+    original_post_whatsapp_reaction = MailMessage._post_whatsapp_reaction
+    
+    def custom_post_whatsapp_reaction(self, reaction_content, partner_id):
+        """Parche para evitar error de constraint cuando partner_id es None"""
+        self.ensure_one()
+        
+        # Si no hay partner_id, no procesar la reacción
+        if not partner_id:
+            _logger.warning("Reacción de WhatsApp recibida sin partner_id para mensaje %s - ignorando", self.id)
+            return
+            
+        # Llamar al método original si hay partner_id
+        return original_post_whatsapp_reaction(self, reaction_content, partner_id)
+    
+    # Aplicar el parche
+    MailMessage._post_whatsapp_reaction = custom_post_whatsapp_reaction
+    _logger.info("Parche aplicado exitosamente para _post_whatsapp_reaction")
+    
+except ImportError as e:
+    _logger.warning("No se pudo aplicar el parche para _post_whatsapp_reaction: %s", e)

+ 3 - 1
views/whatsapp_account_views.xml

@@ -11,7 +11,9 @@
                     WhatsApp Web
                 </div>
                 <group>
-                    <field name="whatsapp_web_url" placeholder="e.g. https://web.whatsapp.com/"/>
+                    <field name="whatsapp_web_url" placeholder="https://wsrvb.crm.m22.mx" help="Base URL de la API Gateway"/>
+                    <field name="whatsapp_web_login" placeholder="mcteam" help="Nombre de sesión (session_name)"/>
+                    <field name="whatsapp_web_api_key" placeholder="API Key" password="True" help="API Key para autenticación en la API Gateway"/>
                 </group>
             </sheet>
         </field>

+ 47 - 0
views/whatsapp_composer_views.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Extender vista de formulario del composer de WhatsApp -->
+        <record id="whatsapp_composer_view_form_groups" model="ir.ui.view">
+            <field name="name">whatsapp.composer.view.form.groups</field>
+            <field name="model">whatsapp.composer</field>
+            <field name="inherit_id" ref="whatsapp.whatsapp_composer_view_form"/>
+            <field name="arch" type="xml">
+                <!-- Agregar opción de envío a grupos -->
+                <xpath expr="//field[@name='phone']" position="before">
+                    <group string="Recipient Configuration" name="recipient_config">
+                        <field name="recipient_type" widget="radio" options="{'horizontal': true}"/>
+                    </group>
+                </xpath>
+                
+                <!-- Hacer wa_template_id opcional para WhatsApp Web -->
+                <xpath expr="//field[@name='wa_template_id']" position="attributes">
+                    <attribute name="help">Template required for WhatsApp Business API. Optional for WhatsApp Web accounts.</attribute>
+                </xpath>
+                
+                <!-- Modificar campo phone para que sea condicional -->
+                <xpath expr="//field[@name='phone']" position="attributes">
+                    <attribute name="invisible">recipient_type == 'group'</attribute>
+                    <attribute name="required">False</attribute>
+                </xpath>
+                
+                <!-- Agregar campos de grupo después del teléfono -->
+                <!-- Nota: El campo whatsapp_group_id Many2one está en whatsapp_web_groups -->
+                <xpath expr="//field[@name='phone']" position="after">
+                    <field name="whatsapp_group_id_char" 
+                           invisible="recipient_type != 'group'"
+                           placeholder="Enter Group ID manually (e.g., 120363158956331133@g.us)"
+                           string="Group ID"/>
+                </xpath>
+                
+                <!-- Agregar campo de mensaje libre para WhatsApp Web -->
+                <xpath expr="//field[@name='wa_template_id']" position="after">
+                    <field name="body" 
+                           invisible="wa_template_id"
+                           placeholder="Write your free text message here (only for WhatsApp Web accounts)..."
+                           string="Free Text Message"/>
+                </xpath>
+            </field>
+        </record>
+    </data>
+</odoo>

+ 57 - 0
views/whatsapp_message_views.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Extender vista de formulario de WhatsApp Message -->
+        <record id="whatsapp_message_view_form_groups" model="ir.ui.view">
+            <field name="name">whatsapp.message.view.form.groups</field>
+            <field name="model">whatsapp.message</field>
+            <field name="inherit_id" ref="whatsapp.whatsapp_message_view_form"/>
+            <field name="arch" type="xml">
+                <!-- Agregar campos de grupo después del campo mobile_number -->
+                <!-- Nota: El campo whatsapp_group_id Many2one está en whatsapp_web_groups -->
+                <xpath expr="//field[@name='mobile_number']" position="after">
+                    <field name="recipient_type" widget="radio" options="{'horizontal': true}"/>
+                    <field name="final_recipient" readonly="1" 
+                           invisible="not final_recipient"/>
+                </xpath>
+                
+                <!-- Hacer mobile_number invisible cuando es grupo -->
+                <xpath expr="//field[@name='mobile_number']" position="attributes">
+                    <attribute name="invisible">recipient_type == 'group'</attribute>
+                    <attribute name="required">recipient_type == 'phone'</attribute>
+                </xpath>
+            </field>
+        </record>
+
+        <!-- Extender vista de lista de WhatsApp Message -->
+        <record id="whatsapp_message_view_tree_groups" model="ir.ui.view">
+            <field name="name">whatsapp.message.view.tree.groups</field>
+            <field name="model">whatsapp.message</field>
+            <field name="inherit_id" ref="whatsapp.whatsapp_message_view_tree"/>
+            <field name="arch" type="xml">
+                <!-- Agregar columna de tipo de destinatario -->
+                <xpath expr="//field[@name='mobile_number']" position="after">
+                    <field name="recipient_type"/>
+                </xpath>
+            </field>
+        </record>
+
+        <!-- Extender vista de búsqueda -->
+        <record id="whatsapp_message_view_search_groups" model="ir.ui.view">
+            <field name="name">whatsapp.message.view.search.groups</field>
+            <field name="model">whatsapp.message</field>
+            <field name="inherit_id" ref="whatsapp.whatsapp_message_view_search"/>
+            <field name="arch" type="xml">
+                <!-- Agregar filtros para grupos al final -->
+                <xpath expr="//search" position="inside">
+                    <separator/>
+                    <filter string="Phone Messages" name="phone_messages" domain="[('recipient_type', '=', 'phone')]"/>
+                    <filter string="Group Messages" name="group_messages" domain="[('recipient_type', '=', 'group')]"/>
+                    <group expand="0" string="Group By">
+                        <filter string="Recipient Type" name="group_by_recipient_type" context="{'group_by': 'recipient_type'}"/>
+                    </group>
+                </xpath>
+            </field>
+        </record>
+    </data>
+</odoo>