Kaynağa Gözat

Squashed 'whatsapp_web/' content from commit 0135327

git-subtree-dir: whatsapp_web
git-subtree-split: 0135327dc1e03021a1849e2595d5753107e3fb56
odoo 2 ay önce
işleme
90ddb31676

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

+ 1 - 0
__init__.py

@@ -0,0 +1 @@
+from . import models

+ 20 - 0
__manifest__.py

@@ -0,0 +1,20 @@
+{
+    'name': 'WhatsApp Web',
+    'version': '1.0',
+    'category': 'Marketing/Marketing',
+    'summary': 'Integra WhatsApp con la automatización de marketing',
+    'description': """
+        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.
+    """,
+    'author': 'Tu Nombre',
+    'website': 'https://www.tuempresa.com',
+    'depends': ['whatsapp', 'marketing_automation_whatsapp'],
+    'data': [
+        'views/whatsapp_account_views.xml',
+        'views/whatsapp_message_views.xml',
+        'views/whatsapp_composer_views.xml'
+    ],
+    'installable': True,
+    'auto_install': False,
+}

+ 5 - 0
models/__init__.py

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

+ 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)}")
+

+ 18 - 0
models/res_config_settings.py

@@ -0,0 +1,18 @@
+from odoo import models, fields
+
+class ResConfigSettings(models.TransientModel):
+    _inherit = 'res.config.settings'
+
+    module_whatsapp_web = fields.Boolean("Activar WhatsApp Marketing")
+    whatsapp_endpoint = fields.Char(string="URL de envío de WhatsApp", config_parameter="whatsapp_web.whatsapp_endpoint")
+
+    def set_values(self):
+        super(ResConfigSettings, self).set_values()
+        self.env['ir.config_parameter'].sudo().set_param('whatsapp_web.whatsapp_endpoint', self.whatsapp_endpoint)
+
+    def get_values(self):
+        res = super(ResConfigSettings, self).get_values()
+        res.update(
+            whatsapp_endpoint=self.env['ir.config_parameter'].sudo().get_param('whatsapp_web.whatsapp_endpoint')
+        )
+        return res

+ 59 - 0
models/whatsapp_account.py

@@ -0,0 +1,59 @@
+import logging
+import requests
+import json
+
+from odoo import fields, models
+
+_logger = logging.getLogger(__name__)
+
+class WhatsAppAccount(models.Model):
+    _inherit = 'whatsapp.account'
+
+    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

+ 333 - 0
models/whatsapp_message.py

@@ -0,0 +1,333 @@
+from odoo import models, fields, api
+from odoo.tools import groupby
+from odoo.exceptions import ValidationError
+import logging
+import markupsafe
+import requests
+import json
+import time
+import random
+import re
+import html
+import base64
+
+_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
+            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 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:
+            # Determinar destinatario final usando solo la nueva lógica
+            final_destination = whatsapp_message._get_final_destination()
+            
+            if final_destination:
+                group = final_destination
+            
+            attachment = False
+
+            if whatsapp_message.wa_template_id:
+                record = self.env[whatsapp_message.wa_template_id.model].browse(whatsapp_message.mail_message_id.res_id)
+                #codigo con base a whatsapp.message y whatsapp.template para generacion de adjuntos
+                RecordModel = self.env[whatsapp_message.mail_message_id.model].with_user(whatsapp_message.create_uid)
+                from_record = RecordModel.browse(whatsapp_message.mail_message_id.res_id)
+
+                # if retrying message then we need to unlink previous attachment
+                # in case of header with report in order to generate it again
+                if whatsapp_message.wa_template_id.report_id and whatsapp_message.wa_template_id.header_type == 'document' and whatsapp_message.mail_message_id.attachment_ids:
+                    whatsapp_message.mail_message_id.attachment_ids.unlink()
+
+                if not attachment and whatsapp_message.wa_template_id.report_id:
+                    attachment = whatsapp_message.wa_template_id._generate_attachment_from_report(record)
+                if not attachment and whatsapp_message.wa_template_id.header_attachment_ids:
+                    attachment = whatsapp_message.wa_template_id.header_attachment_ids[0]
+                
+                if attachment and attachment not in whatsapp_message.mail_message_id.attachment_ids:
+                    whatsapp_message.mail_message_id.attachment_ids = [(4, attachment.id)]
+             # no template
+            elif whatsapp_message.mail_message_id.attachment_ids:
+                attachment = whatsapp_message.mail_message_id.attachment_ids[0]
+
+            #codigo para limpiar body y numero
+            body = whatsapp_message.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)
+                text = re.sub(r'</p>|</P>', '', text)
+                
+                # Eliminamos el resto de etiquetas HTML
+                text = re.sub(r'<[^>]+>', '', text)
+                
+                # Limpiamos múltiples saltos de línea
+                text = re.sub(r'\n\s*\n\s*\n', '\n\n', text)
+                
+                # 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"
+
+            # 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('-','')
+
+                        if number.startswith("52") and len(number) == 12:
+                            number = "521" + number[2:]
+                        
+                        if len(number) == 10:
+                            number = "521" + number
+                        
+                        number = number + '@c.us'
+
+            # 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 = {
+                    "phone": [number],  # Array para send-image/send-file
+                    "base64": base64_with_prefix,
+                    "filename": attachment.name or "file",
+                    "caption": body,
+                    "isGroup": is_group
+                }
+                
+                if parent_message_id:
+                    payload["quotedMessageId"] = parent_message_id
+            else:
+                # Mensaje de texto
+                payload = {
+                    "phone": number,  # String para send-message
+                    "message": body,
+                    "isGroup": is_group
+                }
+                
+                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': 'error'
+                    })
+                    self._cr.commit()
+            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))

+ 49 - 0
models/whatsapp_patch.py

@@ -0,0 +1,49 @@
+import logging
+import base64
+from odoo.addons.whatsapp.tools.whatsapp_api import WhatsAppApi
+
+_logger = logging.getLogger(__name__)
+
+# Guarda una referencia al método original
+original_get_whatsapp_document = WhatsAppApi._get_whatsapp_document
+
+def custom_get_whatsapp_document(self, document_id):
+    _logger.info("Ejecutando versión modificada de _get_whatsapp_document")
+
+    if self.wa_account_id.whatsapp_web_url: 
+        _logger.info("Ejecutando versión modificada de _get_whatsapp_document con whatsapp web")
+        result = base64.b64decode(document_id)
+    else:
+        result = original_get_whatsapp_document(self, document_id)
+
+    # Aquí puedes modificar 'result' si es necesario antes de devolverlo
+    return result
+
+# 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)

+ 2 - 0
security/ir.model.access.csv

@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_whatsapp_web_config,access_whatsapp_web_config,model_res_config_settings,base.group_system,1,1,1,1

+ 22 - 0
views/res_config_settings_views.xml

@@ -0,0 +1,22 @@
+<odoo>
+    <record id="view_res_config_settings_form_inherit_whatsapp" model="ir.ui.view">
+        <field name="name">res.config.settings.whatsapp.marketing.form</field>
+        <field name="model">res.config.settings</field>
+        <field name="inherit_id" ref="base.res_config_settings_view_form"/>
+        <field name="arch" type="xml">
+            <xpath expr="//form" position="inside">
+                <div class="mt16">
+                    <h2>WhatsApp Marketing</h2>
+                    <div class="row">
+                        <div class="col-12 col-lg-6">
+                            <field name="module_whatsapp_web"/>
+                        </div>
+                        <div class="col-12 col-lg-6">
+                            <field name="whatsapp_endpoint"/>
+                        </div>
+                    </div>
+                </div>
+            </xpath>
+        </field>
+    </record>
+</odoo>

+ 21 - 0
views/whatsapp_account_views.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <record id="whatsapp_account_view_form_custom" model="ir.ui.view">
+        <field name="name">whatsapp.account.view.form.custom</field>
+        <field name="model">whatsapp.account</field>
+        <field name="inherit_id" ref="whatsapp.whatsapp_account_view_form"/>
+        <field name="arch" type="xml">
+            <!-- Agregamos una nueva sección al final del <sheet> -->
+            <sheet position="inside">
+                <div class="o_horizontal_separator mt-4 mb-3 text-uppercase fw-bolder small">
+                    WhatsApp Web
+                </div>
+                <group>
+                    <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>
+    </record>
+</odoo>

+ 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>