Parcourir la source

Add WhatsApp groups management features and documentation

- Add marketing activity integration for group messaging
- Add comprehensive API reference documentation
- Add detailed README with installation and usage guide
- Enhance group model with better contact handling
- Update group views for better UI
- Add marketing activity views for WhatsApp group selection
root il y a 4 mois
Parent
commit
bc6d6f7aa3
8 fichiers modifiés avec 1186 ajouts et 19 suppressions
  1. 583 0
      API_REFERENCE.md
  2. 386 0
      README.md
  3. 2 0
      __manifest__.py
  4. 2 1
      models/__init__.py
  5. 99 0
      models/marketing_activity.py
  6. 76 7
      models/ww_group.py
  7. 19 0
      views/marketing_activity_views.xml
  8. 19 11
      views/ww_group_views.xml

+ 583 - 0
API_REFERENCE.md

@@ -0,0 +1,583 @@
+# API Reference - Gestor de Grupos WhatsApp Web
+
+## Índice
+- [Modelos](#modelos)
+- [Métodos](#métodos)
+- [Campos](#campos)
+- [Ejemplos de Uso](#ejemplos-de-uso)
+- [Respuestas de API](#respuestas-de-api)
+
+## Modelos
+
+### ww.group
+
+Modelo principal para gestión de grupos de WhatsApp Web.
+
+#### Campos
+
+| Campo | Tipo | Descripción |
+|-------|------|-------------|
+| `name` | Char | Nombre del grupo (requerido) |
+| `whatsapp_web_id` | Char | ID único del grupo en WhatsApp Web |
+| `whatsapp_account_id` | Many2one | Cuenta de WhatsApp asociada |
+| `channel_id` | Many2one | Canal de discusión creado |
+| `contact_ids` | Many2many | Contactos miembros del grupo |
+
+#### Métodos
+
+##### `_process_messages(messages_data)`
+Procesa mensajes de WhatsApp y los crea en el canal de discusión.
+
+**Parámetros:**
+- `messages_data` (list): Lista de mensajes de WhatsApp
+
+**Retorna:** `bool` - True si se procesó correctamente
+
+**Ejemplo:**
+```python
+group = self.env['ww.group'].browse(1)
+messages = [
+    {
+        'id': {'_serialized': '3EB0C767D26A3D1B7B4A'},
+        'body': 'Hola grupo!',
+        'author': '5215551234567@c.us',
+        'timestamp': 1640995200,
+        'hasQuotedMsg': False
+    }
+]
+result = group._process_messages(messages)
+```
+
+##### `_create_discussion_channel()`
+Crea un canal de discusión para el grupo.
+
+**Parámetros:** Ninguno
+
+**Retorna:** `discuss.channel|False` - Canal creado o False si falla
+
+**Ejemplo:**
+```python
+group = self.env['ww.group'].browse(1)
+channel = group._create_discussion_channel()
+if channel:
+    print(f"Canal creado: {channel.name}")
+```
+
+##### `_update_discussion_channel()`
+Actualiza los miembros del canal de discusión.
+
+**Parámetros:** Ninguno
+
+**Retorna:** `discuss.channel|False` - Canal actualizado o False si falla
+
+**Ejemplo:**
+```python
+group = self.env['ww.group'].browse(1)
+channel = group._update_discussion_channel()
+```
+
+##### `sync_ww_contacts_groups()`
+Sincroniza todos los grupos y contactos desde WhatsApp Web.
+
+**Parámetros:** Ninguno (método de modelo)
+
+**Retorna:** `bool` - True si se sincronizó correctamente
+
+**Ejemplo:**
+```python
+# Sincronización completa
+self.env['ww.group'].sync_ww_contacts_groups()
+
+# Sincronización desde un grupo específico
+group = self.env['ww.group'].browse(1)
+# (Este método es estático, no se llama desde instancia)
+```
+
+##### `send_whatsapp_message(body, attachment=None, wa_template_id=None)`
+Envía un mensaje WhatsApp al grupo.
+
+**Parámetros:**
+- `body` (str): Contenido del mensaje
+- `attachment` (ir.attachment, opcional): Archivo adjunto
+- `wa_template_id` (whatsapp.template, opcional): Plantilla WhatsApp
+
+**Retorna:** `whatsapp.message` - Mensaje creado y enviado
+
+**Ejemplo:**
+```python
+group = self.env['ww.group'].browse(1)
+attachment = self.env['ir.attachment'].browse(1)
+template = self.env['whatsapp.template'].browse(1)
+
+message = group.send_whatsapp_message(
+    body="Mensaje importante para el grupo",
+    attachment=attachment,
+    wa_template_id=template
+)
+```
+
+##### `action_send_whatsapp_message()`
+Abre el composer de WhatsApp para enviar mensaje al grupo.
+
+**Parámetros:** Ninguno
+
+**Retorna:** `dict` - Acción de ventana para abrir composer
+
+**Ejemplo:**
+```python
+group = self.env['ww.group'].browse(1)
+action = group.action_send_whatsapp_message()
+# Retorna acción para abrir ventana del composer
+```
+
+---
+
+### ww.contact (Extiende res.partner)
+
+Extensión del modelo de contactos para WhatsApp Web.
+
+#### Campos Adicionales
+
+| Campo | Tipo | Descripción |
+|-------|------|-------------|
+| `whatsapp_web_id` | Char | ID único del contacto en WhatsApp Web |
+| `group_ids` | Many2many | Grupos donde participa el contacto |
+
+#### Relaciones
+
+- **group_ids**: Relación many2many con `ww.group`
+- **channel_ids**: Relación con canales de discusión
+- **meeting_ids**: Relación con eventos de calendario
+- **sla_ids**: Relación con SLAs de helpdesk
+
+---
+
+### ww.role
+
+Modelo para roles de miembros en grupos.
+
+#### Campos
+
+| Campo | Tipo | Descripción |
+|-------|------|-------------|
+| `name` | Char | Nombre del rol (requerido) |
+| `description` | Text | Descripción del rol |
+
+---
+
+### ww.group_contact_rel
+
+Modelo de relación entre grupos y contactos con información adicional.
+
+#### Campos
+
+| Campo | Tipo | Descripción |
+|-------|------|-------------|
+| `group_id` | Many2one | Referencia al grupo |
+| `contact_id` | Many2one | Referencia al contacto |
+| `is_admin` | Boolean | Si es administrador del grupo |
+| `is_super_admin` | Boolean | Si es super administrador |
+| `role_id` | Many2one | Rol asignado en el grupo |
+
+#### Constraints
+
+- **group_contact_uniq**: Constraint único para evitar duplicados (group_id, contact_id)
+
+---
+
+## Ejemplos de Uso
+
+### Sincronización de Grupos
+
+```python
+# Sincronización completa
+self.env['ww.group'].sync_ww_contacts_groups()
+
+# Verificar grupos sincronizados
+groups = self.env['ww.group'].search([])
+for group in groups:
+    print(f"Grupo: {group.name}")
+    print(f"Miembros: {len(group.contact_ids)}")
+    print(f"Canal: {group.channel_id.name if group.channel_id else 'Sin canal'}")
+```
+
+### Creación Manual de Grupo
+
+```python
+# Crear grupo manualmente
+group = self.env['ww.group'].create({
+    'name': 'Mi Grupo Personalizado',
+    'whatsapp_web_id': '120363158956331133@g.us',
+    'whatsapp_account_id': account.id
+})
+
+# Agregar contactos al grupo
+contacts = self.env['res.partner'].search([('whatsapp_web_id', '!=', False)])
+group.contact_ids = [(6, 0, contacts.ids)]
+
+# Crear canal de discusión
+channel = group._create_discussion_channel()
+```
+
+### Gestión de Contactos
+
+```python
+# Buscar contactos de WhatsApp Web
+contacts = self.env['res.partner'].search([
+    ('whatsapp_web_id', '!=', False)
+])
+
+# Agregar contacto a grupo
+group = self.env['ww.group'].browse(1)
+contact = self.env['res.partner'].browse(1)
+
+# Crear relación con rol
+rel = self.env['ww.group_contact_rel'].create({
+    'group_id': group.id,
+    'contact_id': contact.id,
+    'is_admin': True,
+    'role_id': admin_role.id
+})
+```
+
+### Envío de Mensajes a Grupos
+
+```python
+# Envío directo
+group = self.env['ww.group'].browse(1)
+message = group.send_whatsapp_message("Mensaje importante!")
+
+# Envío con adjunto
+attachment = self.env['ir.attachment'].create({
+    'name': 'documento.pdf',
+    'type': 'binary',
+    'datas': base64.b64encode(pdf_content),
+    'mimetype': 'application/pdf'
+})
+
+message = group.send_whatsapp_message(
+    body="Adjunto documento importante",
+    attachment=attachment
+)
+
+# Envío usando composer
+action = group.action_send_whatsapp_message()
+```
+
+### Procesamiento de Mensajes
+
+```python
+# Procesar mensajes de un grupo
+group = self.env['ww.group'].browse(1)
+messages_data = [
+    {
+        'id': {'_serialized': 'msg_id_123'},
+        'body': 'Mensaje de prueba',
+        'author': '5215551234567@c.us',
+        'timestamp': 1640995200,
+        'hasQuotedMsg': True,
+        'quotedMsg': {
+            'body': 'Mensaje citado',
+            'author': '5215557654321@c.us'
+        }
+    }
+]
+
+result = group._process_messages(messages_data)
+```
+
+### Gestión de Roles
+
+```python
+# Crear roles
+admin_role = self.env['ww.role'].create({
+    'name': 'Administrador',
+    'description': 'Administrador del grupo'
+})
+
+member_role = self.env['ww.role'].create({
+    'name': 'Miembro',
+    'description': 'Miembro regular del grupo'
+})
+
+# Asignar roles a contactos en grupos
+group = self.env['ww.group'].browse(1)
+for contact in group.contact_ids:
+    rel = self.env['ww.group_contact_rel'].search([
+        ('group_id', '=', group.id),
+        ('contact_id', '=', contact.id)
+    ])
+    
+    if contact.is_admin:
+        rel.role_id = admin_role.id
+    else:
+        rel.role_id = member_role.id
+```
+
+### Consultas Avanzadas
+
+```python
+# Grupos con más de 10 miembros
+large_groups = self.env['ww.group'].search([
+    ('contact_ids', '!=', False)
+])
+large_groups = [g for g in large_groups if len(g.contact_ids) > 10]
+
+# Contactos administradores
+admin_contacts = self.env['res.partner'].search([
+    ('group_ids', '!=', False)
+])
+admin_contacts = [c for c in admin_contacts 
+                  if any(rel.is_admin for rel in c.group_ids)]
+
+# Grupos sin canal
+groups_without_channel = self.env['ww.group'].search([
+    ('channel_id', '=', False)
+])
+```
+
+## Respuestas de API
+
+### Respuesta de getGroups()
+
+```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,
+                "quotedParticipant": null,
+                "quotedMsg": null
+            }
+        ]
+    }
+]
+```
+
+### Respuesta de Creación de Canal
+
+```python
+# Respuesta exitosa
+{
+    'id': 123,
+    'name': '📱 Mi Grupo de Trabajo',
+    'channel_type': 'channel',
+    'member_count': 5
+}
+
+# Respuesta de error (False)
+False
+```
+
+### Respuesta de Envío de Mensaje
+
+```python
+# Mensaje enviado exitosamente
+{
+    'id': 456,
+    'state': 'sent',
+    'msg_uid': '3EB0C767D26A3D1B7B4A_120363158956331133@g.us',
+    'body': 'Mensaje importante!',
+    'recipient_type': 'group'
+}
+```
+
+## Manejo de Errores
+
+### Excepciones Comunes
+
+#### ValueError - Grupo sin cuenta
+```python
+try:
+    group.send_whatsapp_message("Mensaje")
+except ValueError as e:
+    print(f"Error: {e}")  # "Group must have a WhatsApp account configured"
+```
+
+#### ValidationError - Datos inválidos
+```python
+from odoo.exceptions import ValidationError
+
+try:
+    group = self.env['ww.group'].create({
+        'name': '',  # Nombre vacío
+        'whatsapp_web_id': 'invalid_id'
+    })
+except ValidationError as e:
+    print(f"Error de validación: {e}")
+```
+
+### Logs de Debugging
+
+```python
+import logging
+_logger = logging.getLogger(__name__)
+
+# Log de sincronización
+_logger.info(f"Procesando grupo {group_name}: {len(participants)} participantes encontrados")
+
+# Log de error
+_logger.error("Error en la sincronización de grupos para la cuenta %s: %s", 
+              account.name, str(e))
+
+# Log de advertencia
+_logger.warning(f"Grupo {group_name} no tiene participantes en la respuesta de la API")
+```
+
+## Configuración de Cron Jobs
+
+### Configuración Básica
+
+```xml
+<record id="ir_cron_sync_ww_contacts_groups" model="ir.cron">
+    <field name="name">Sincronizar Contactos y Grupos WhatsApp Web</field>
+    <field name="model_id" ref="model_ww_group"/>
+    <field name="state">code</field>
+    <field name="code">model.sync_ww_contacts_groups()</field>
+    <field name="interval_number">1</field>
+    <field name="interval_type">hours</field>
+    <field name="active" eval="False"/>
+</record>
+```
+
+### Configuración Personalizada
+
+```python
+# Crear cron job personalizado
+cron = self.env['ir.cron'].create({
+    'name': 'Sincronización Personalizada',
+    'model_id': self.env.ref('whatsapp_web_groups.model_ww_group').id,
+    'state': 'code',
+    'code': 'model.sync_ww_contacts_groups()',
+    'interval_number': 2,
+    'interval_type': 'hours',
+    'active': True,
+    'user_id': self.env.user.id
+})
+```
+
+## Optimización de Performance
+
+### Sincronización en Lotes
+
+```python
+# Procesar grupos en lotes para evitar timeouts
+def sync_groups_in_batches(self, batch_size=10):
+    accounts = self.env['whatsapp.account'].search([])
+    
+    for account in accounts:
+        groups_data = account.get_groups()
+        
+        # Procesar en lotes
+        for i in range(0, len(groups_data), batch_size):
+            batch = groups_data[i:i + batch_size]
+            self._process_groups_batch(batch, account)
+            
+            # Commit después de cada lote
+            self._cr.commit()
+```
+
+### Creación Bulk de Mensajes
+
+```python
+def _process_messages_bulk(self, messages_data):
+    """Procesar mensajes en bulk para mejor performance"""
+    if not messages_data:
+        return True
+    
+    # Preparar valores para creación bulk
+    message_vals_list = []
+    for msg_data in messages_data:
+        message_vals = self._prepare_message_vals(msg_data)
+        message_vals_list.append(message_vals)
+    
+    # Crear todos los mensajes de una vez
+    if message_vals_list:
+        self.env['mail.message'].create(message_vals_list)
+    
+    return True
+```
+
+## Limpieza y Mantenimiento
+
+### Limpiar Grupos Vacíos
+
+```python
+def cleanup_empty_groups(self):
+    """Eliminar grupos sin contactos"""
+    empty_groups = self.env['ww.group'].search([
+        ('contact_ids', '=', False)
+    ])
+    
+    if empty_groups:
+        _logger.info(f"Eliminando {len(empty_groups)} grupos vacíos")
+        empty_groups.unlink()
+```
+
+### Limpiar Contactos Huérfanos
+
+```python
+def cleanup_orphan_contacts(self):
+    """Limpiar contactos sin grupos"""
+    orphan_contacts = self.env['res.partner'].search([
+        ('whatsapp_web_id', '!=', False),
+        ('group_ids', '=', False)
+    ])
+    
+    if orphan_contacts:
+        _logger.info(f"Limpiando {len(orphan_contacts)} contactos huérfanos")
+        orphan_contacts.write({'whatsapp_web_id': False})
+```
+
+### Regenerar Canales Perdidos
+
+```python
+def regenerate_missing_channels(self):
+    """Regenerar canales que se perdieron"""
+    groups_without_channel = self.env['ww.group'].search([
+        ('channel_id', '=', False),
+        ('contact_ids', '!=', False)
+    ])
+    
+    for group in groups_without_channel:
+        try:
+            channel = group._create_discussion_channel()
+            if channel:
+                _logger.info(f"Canal regenerado para grupo {group.name}")
+        except Exception as e:
+            _logger.error(f"Error regenerando canal para {group.name}: {e}")
+```
+

+ 386 - 0
README.md

@@ -0,0 +1,386 @@
+# Gestor de Grupos de WhatsApp Web para Odoo 18
+
+## Descripción
+
+Este módulo permite gestionar grupos, contactos y roles de WhatsApp Web, sincronizando automáticamente con la API de whatsapp-web.js. Proporciona una interfaz completa para administrar grupos de WhatsApp y sus miembros dentro de Odoo.
+
+## Características Principales
+
+- ✅ Sincronización automática de grupos de WhatsApp Web
+- ✅ Gestión de contactos y miembros de grupos
+- ✅ Creación automática de canales de discusión para cada grupo
+- ✅ Sistema de roles y permisos para miembros
+- ✅ Integración con el sistema de contactos de Odoo
+- ✅ Procesamiento automático de mensajes de grupos
+- ✅ Sincronización programada con cron jobs
+- ✅ Envío directo de mensajes a grupos
+
+## Requisitos
+
+- Odoo 18.0
+- Módulo `whatsapp_web` (dependencia obligatoria)
+- Módulos: `base`, `contacts`, `mail`, `calendar`, `helpdesk`
+- Servidor whatsapp-web.js configurado y funcionando
+
+## Instalación
+
+1. **Instalar dependencias:**
+   ```bash
+   cd /var/odoo/mcteam.run
+   sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -i whatsapp_web
+   ```
+
+2. **Instalar el módulo:**
+   ```bash
+   sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -i whatsapp_web_groups
+   ```
+
+3. **Reiniciar el servidor Odoo:**
+   ```bash
+   ./restart_odoo.sh
+   ```
+
+## Configuración Inicial
+
+### 1. Configurar Cuenta WhatsApp Web
+
+1. Ir a **WhatsApp > Configuración > Cuentas WhatsApp**
+2. Crear o editar una cuenta WhatsApp
+3. Configurar la **WhatsApp Web URL** (ej: `https://web.whatsapp.com/api`)
+4. Guardar la configuración
+
+### 2. Sincronización Inicial
+
+1. Ir a **WhatsApp Web > Grupos**
+2. Hacer clic en "Sincronizar" o ejecutar manualmente:
+   ```python
+   self.env['ww.group'].sync_ww_contacts_groups()
+   ```
+
+### 3. Configurar Cron Job (Opcional)
+
+Para sincronización automática cada hora:
+
+1. Ir a **Configuración > Técnico > Automatización > Acciones Programadas**
+2. Buscar "Sincronizar Contactos y Grupos WhatsApp Web"
+3. Activar el cron job
+4. Configurar intervalo deseado
+
+## Uso
+
+### Gestión de Grupos
+
+#### Ver Grupos Sincronizados
+1. Ir a **WhatsApp Web > Grupos**
+2. Ver lista de todos los grupos sincronizados
+3. Hacer clic en un grupo para ver detalles
+
+#### Enviar Mensaje a Grupo
+1. Seleccionar un grupo de la lista
+2. Hacer clic en "Send WhatsApp Message"
+3. Completar el composer de WhatsApp
+4. Enviar el mensaje
+
+#### Sincronización Manual
+```python
+# Desde shell de Odoo
+groups = self.env['ww.group']
+groups.sync_ww_contacts_groups()
+```
+
+### Gestión de Contactos
+
+#### Ver Contactos WhatsApp
+1. Ir a **Contactos > Contactos WhatsApp Web**
+2. Ver todos los contactos sincronizados de WhatsApp
+3. Editar información de contactos si es necesario
+
+#### Contactos en Grupos
+- Los contactos se sincronizan automáticamente cuando están en grupos
+- Se actualizan los números de teléfono y nombres
+- Se evitan duplicados usando los últimos 10 dígitos del móvil
+
+### Canales de Discusión
+
+#### Creación Automática
+- Cada grupo crea automáticamente un canal de discusión
+- El canal se nombra con prefijo "📱" + nombre del grupo
+- Todos los miembros del grupo se agregan al canal
+
+#### Gestión de Canales
+1. Ir al canal desde el menú de discusión
+2. Los mensajes de WhatsApp se procesan automáticamente
+3. Se mantiene historial de conversaciones
+
+## Estructura de Datos
+
+### Modelos Principales
+
+#### `ww.group`
+Representa un grupo de WhatsApp Web.
+
+**Campos:**
+- `name`: Nombre del grupo
+- `whatsapp_web_id`: ID único en WhatsApp Web
+- `whatsapp_account_id`: Cuenta WhatsApp asociada
+- `channel_id`: Canal de discusión creado
+- `contact_ids`: Contactos miembros del grupo
+
+#### `ww.contact` (Extiende `res.partner`)
+Contactos de WhatsApp Web.
+
+**Campos adicionales:**
+- `whatsapp_web_id`: ID único en WhatsApp Web
+- `group_ids`: Grupos donde participa el contacto
+
+#### `ww.role`
+Roles que pueden tener los miembros en grupos.
+
+**Campos:**
+- `name`: Nombre del rol
+- `description`: Descripción del rol
+
+#### `ww.group_contact_rel`
+Relación entre grupos y contactos con roles.
+
+**Campos:**
+- `group_id`: Referencia al grupo
+- `contact_id`: Referencia al contacto
+- `is_admin`: Si es administrador
+- `is_super_admin`: Si es super administrador
+- `role_id`: Rol asignado
+
+## API del Módulo
+
+### Métodos Principales
+
+#### `ww.group.sync_ww_contacts_groups()`
+Sincroniza todos los grupos y contactos desde WhatsApp Web.
+
+```python
+# Sincronización completa
+self.env['ww.group'].sync_ww_contacts_groups()
+
+# Sincronización por cuenta específica
+account = self.env['whatsapp.account'].browse(1)
+groups_data = account.get_groups()
+```
+
+#### `ww.group._create_discussion_channel()`
+Crea un canal de discusión para el grupo.
+
+```python
+group = self.env['ww.group'].browse(1)
+channel = group._create_discussion_channel()
+```
+
+#### `ww.group._process_messages(messages_data)`
+Procesa mensajes de WhatsApp y los crea en el canal.
+
+```python
+group = self.env['ww.group'].browse(1)
+messages = [
+    {
+        'id': {'_serialized': 'message_id'},
+        'body': 'Contenido del mensaje',
+        'author': 'contacto_id',
+        'timestamp': 1234567890
+    }
+]
+group._process_messages(messages)
+```
+
+#### `ww.group.send_whatsapp_message(body, attachment=None, wa_template_id=None)`
+Envía un mensaje WhatsApp al grupo.
+
+```python
+group = self.env['ww.group'].browse(1)
+message = group.send_whatsapp_message(
+    body="Hola grupo!",
+    attachment=attachment_record,
+    wa_template_id=template_record
+)
+```
+
+#### `ww.group.action_send_whatsapp_message()`
+Abre el composer de WhatsApp para enviar mensaje al grupo.
+
+```python
+group = self.env['ww.group'].browse(1)
+action = group.action_send_whatsapp_message()
+```
+
+### Métodos de Sincronización
+
+#### `whatsapp.account.get_groups()`
+Obtiene grupos desde el servidor whatsapp-web.js.
+
+**Respuesta esperada:**
+```json
+[
+    {
+        "id": {"_serialized": "120363158956331133@g.us"},
+        "name": "Mi Grupo",
+        "members": [
+            {
+                "id": {"_serialized": "5215551234567@c.us"},
+                "number": "5551234567",
+                "name": "Juan Pérez",
+                "isAdmin": false,
+                "isSuperAdmin": false
+            }
+        ],
+        "messages": [...]
+    }
+]
+```
+
+## Configuración Avanzada
+
+### Personalización de Sincronización
+
+#### Modificar Intervalo de Sincronización
+```xml
+<!-- En data/ir_cron.xml -->
+<field name="interval_number">2</field>
+<field name="interval_type">hours</field>
+```
+
+#### Filtros de Sincronización
+```python
+# Sincronizar solo grupos específicos
+accounts = self.env['whatsapp.account'].search([
+    ('name', 'ilike', 'cuenta_especifica')
+])
+for account in accounts:
+    groups_data = account.get_groups()
+    # Procesar solo grupos deseados
+```
+
+### Gestión de Permisos
+
+#### Configurar Accesos
+```csv
+# En security/ir.model.access.csv
+access_ww_group_user,ww.group.user,model_ww_group,base.group_user,1,1,1,0
+access_ww_group_manager,ww.group.manager,model_ww_group,base.group_system,1,1,1,1
+```
+
+## Solución de Problemas
+
+### Error: "No hay contactos para crear el canal"
+- **Causa**: El grupo no tiene miembros o la sincronización falló
+- **Solución**: Ejecutar sincronización manual y verificar conectividad
+
+### Error: "Error al crear el canal para el grupo"
+- **Causa**: Problemas de permisos o configuración de grupos de usuario
+- **Solución**: Verificar que el usuario tenga permisos para crear canales
+
+### Grupos no se sincronizan
+- **Causa**: Servidor whatsapp-web.js no responde o método `getGroups` no implementado
+- **Solución**: 
+  1. Verificar conectividad con el servidor
+  2. Confirmar implementación del método `getGroups`
+  3. Revisar logs del servidor whatsapp-web.js
+
+### Contactos duplicados
+- **Causa**: Múltiples números similares en diferentes formatos
+- **Solución**: El módulo usa los últimos 10 dígitos para evitar duplicados automáticamente
+
+### Mensajes no se procesan en canales
+- **Causa**: Formato incorrecto de datos de mensajes o canal no creado
+- **Solución**:
+  1. Verificar que el grupo tenga canal asociado
+  2. Confirmar formato de datos de mensajes
+  3. Revisar permisos de escritura en canales
+
+## Logs y Debugging
+
+### Logs de Sincronización
+```bash
+# Ver logs de sincronización
+tail -f /var/odoo/stg2.mcteam.run/logs/odoo-server.log | grep -i "sincroniz\|grupo\|contacto"
+```
+
+### Debug de Grupos
+```python
+# Verificar estado de grupos
+groups = self.env['ww.group'].search([])
+for group in groups:
+    print(f"Grupo: {group.name}")
+    print(f"Contactos: {len(group.contact_ids)}")
+    print(f"Canal: {group.channel_id.name if group.channel_id else 'Sin canal'}")
+```
+
+### Debug de Sincronización
+```python
+# Probar sincronización paso a paso
+account = self.env['whatsapp.account'].search([('whatsapp_web_url', '!=', False)], limit=1)
+groups_data = account.get_groups()
+print(f"Grupos obtenidos: {len(groups_data)}")
+```
+
+## Mantenimiento
+
+### Limpieza de Datos
+```python
+# Limpiar grupos sin contactos
+empty_groups = self.env['ww.group'].search([
+    ('contact_ids', '=', False)
+])
+empty_groups.unlink()
+
+# Limpiar contactos sin grupos
+orphan_contacts = self.env['res.partner'].search([
+    ('whatsapp_web_id', '!=', False),
+    ('group_ids', '=', False)
+])
+orphan_contacts.write({'whatsapp_web_id': False})
+```
+
+### Optimización de Performance
+- La sincronización procesa grupos en lotes para evitar timeouts
+- Los mensajes se crean en bulk para mejorar performance
+- Se evitan duplicados usando constraints de base de datos
+
+## 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_groups
+./restart_odoo.sh
+```
+
+## Integración con Otros Módulos
+
+### Marketing
+- Los grupos se pueden usar como destinatarios en campañas de marketing
+- Soporte para plantillas personalizadas por grupo
+
+### Helpdesk
+- Crear tickets automáticamente desde mensajes de grupos
+- Asignar SLAs basados en roles de grupo
+
+### Calendar
+- Programar reuniones con miembros de grupos
+- Invitaciones automáticas a eventos
+
+## Soporte
+
+Para soporte técnico o reportar bugs:
+- Revisar logs de Odoo y del servidor whatsapp-web.js
+- Verificar configuración de cuentas WhatsApp
+- Confirmar conectividad de red
+
+## Changelog
+
+### Versión 1.0
+- Implementación inicial del gestor de grupos
+- Sincronización automática de grupos y contactos
+- Creación automática de canales de discusión
+- Sistema de roles y permisos
+- Procesamiento de mensajes de grupos
+- Integración con composer de WhatsApp
+

+ 2 - 0
__manifest__.py

@@ -9,6 +9,7 @@
         'base',
         'contacts',
         'whatsapp_web',
+        'marketing_automation_whatsapp',
         'mail',
         'calendar',
         'helpdesk',
@@ -19,6 +20,7 @@
         'views/ww_group_views.xml',
         'views/ww_role_views.xml',
         'views/ww_group_contact_rel_views.xml',
+        'views/marketing_activity_views.xml',
         'data/ir_cron.xml',
     ],
     'installable': True,

+ 2 - 1
models/__init__.py

@@ -1,4 +1,5 @@
 from . import ww_contact
 from . import ww_group
 from . import ww_role
-from . import ww_group_contact_rel 
+from . import ww_group_contact_rel
+from . import marketing_activity 

+ 99 - 0
models/marketing_activity.py

@@ -0,0 +1,99 @@
+from odoo import models, fields
+import logging
+
+_logger = logging.getLogger(__name__)
+
+class MarketingActivity(models.Model):
+    _inherit = 'marketing.activity'
+
+    # Campo para grupo de WhatsApp en actividades de marketing
+    whatsapp_group_id = fields.Many2one(
+        'ww.group', 
+        string='Grupo de WhatsApp',
+        help="Grupo de WhatsApp para enviar mensajes (opcional). Si está vacío, se envían a destinatarios individuales según la plantilla."
+    )
+
+    def _execute_whatsapp(self, traces):
+        """Override para soportar envío a grupos de WhatsApp usando campos nativos"""
+        _logger.info(f"Ejecutando WhatsApp para actividad {self.id}: {self.name}")
+        
+        # Si hay grupo configurado, usar la lógica simple que funcionaba antes
+        if self.whatsapp_group_id:
+            _logger.info(f"Enviando mensaje WhatsApp al grupo NATIVO: {self.whatsapp_group_id.name}")
+            
+            try:
+                # Usar el método original pero con mobile_number del grupo
+                res_ids = [res_id for res_id in set(traces.mapped('res_id')) if res_id]
+                now = self.env.cr.now()
+
+                composer_vals = {
+                    'res_model': self.model_name, 
+                    'res_ids': res_ids,
+                    'wa_template_id': self.whatsapp_template_id.id,
+                    'batch_mode': True,
+                    'phone': self.whatsapp_group_id.whatsapp_web_id,  # ✅ Usar ID del grupo como "teléfono"
+                }
+                
+                _logger.info(f"Usando método original con grupo ID: {self.whatsapp_group_id.whatsapp_web_id}")
+                composer = self.env['whatsapp.composer'].with_context(active_model=self.model_name).create(composer_vals)
+                messages = composer._create_whatsapp_messages(force_create=True)
+                message_by_res_id = {r.mail_message_id.res_id: r for r in messages}
+                
+                # Asignar mensajes a traces (como el original)
+                for trace in self.trace_ids:
+                    res_id = trace.res_id
+                    message = message_by_res_id.get(res_id, self.env['whatsapp.message'])
+                    if message:
+                        trace.whatsapp_message_id = message.id
+                        # Marcar como grupo
+                        message.write({
+                            'recipient_type': 'group',
+                            'whatsapp_group_id': self.whatsapp_group_id.id,
+                        })
+                    if not message.mobile_number:
+                        message.state = 'error'
+                        message.failure_type = 'phone_invalid'
+
+                # Enviar mensajes (como el original)
+                messages._send()
+                    
+                _logger.info(f"Mensaje enviado exitosamente al grupo {self.whatsapp_group_id.name}")
+                    
+            except Exception as e:
+                _logger.warning('Marketing Automation: actividad <%s> error al enviar a grupo WhatsApp %s', self.id, str(e))
+                traces.write({
+                    'state': 'error',
+                    'schedule_date': now,
+                    'state_msg': f'Error al enviar a grupo WhatsApp: {e}',
+                })
+            else:
+                # LÓGICA ORIGINAL: Marcar traces como procesados
+                cancelled_traces = traces.filtered(lambda trace: trace.whatsapp_message_id.state == 'cancel')
+                error_traces = traces.filtered(lambda trace: trace.whatsapp_message_id.state == 'error')
+
+                if cancelled_traces:
+                    cancelled_traces.write({
+                        'state': 'canceled',
+                        'schedule_date': now,
+                        'state_msg': 'WhatsApp canceled'
+                    })
+                if error_traces:
+                    error_traces.write({
+                        'state': 'error',
+                        'schedule_date': now,
+                        'state_msg': 'WhatsApp failed'
+                    })
+
+                processed_traces = traces - (cancelled_traces | error_traces)
+                if processed_traces:
+                    processed_traces.write({
+                        'state': 'processed',
+                        'schedule_date': now,
+                    })
+        else:
+            _logger.info(f"Campo whatsapp_group_id VACÍO - Enviando a destinatarios individuales")
+            # Usar lógica original para envío individual
+            super()._execute_whatsapp(traces)
+        
+        return True
+

+ 76 - 7
models/ww_group.py

@@ -97,7 +97,8 @@ class WWGroup(models.Model):
             
             # Verificar que hay contactos
             if not self.contact_ids:
-                _logger.warning(f"No hay contactos para crear el canal del grupo {self.name}")
+                _logger.warning(f"No hay contactos para crear el canal del grupo {self.name} - saltando creación de canal")
+                # No crear canal pero no fallar, permitir que el grupo exista
                 return False
 
             # Obtener los IDs de los contactos de forma segura
@@ -150,7 +151,8 @@ class WWGroup(models.Model):
                     partner_ids.append(contact.id)
 
             if not partner_ids:
-                _logger.warning(f"No hay contactos válidos para actualizar el canal del grupo {self.name}")
+                _logger.warning(f"No hay contactos válidos para actualizar el canal del grupo {self.name} - saltando actualización")
+                # Si no hay contactos, no actualizar pero no fallar
                 return channel
 
             # Update channel members using add_members
@@ -194,8 +196,8 @@ class WWGroup(models.Model):
                             'whatsapp_web_id': group_id,
                             'whatsapp_account_id': account.id
                         })
-                        # Create discussion channel for new group
-                        group._create_discussion_channel()
+                        # Crear canal solo después de procesar participantes
+                        # Se hará más abajo si hay contact_ids
                     else:
                         # Actualizar nombre del grupo si cambió
                         if group.name != group_name:
@@ -207,6 +209,11 @@ class WWGroup(models.Model):
                     # Procesar participantes del grupo
                     participants = group_data.get('members', [])
                     contact_ids = []
+                    
+                    # Log para debug
+                    _logger.info(f"Procesando grupo {group_name}: {len(participants)} participantes encontrados")
+                    if not participants:
+                        _logger.warning(f"Grupo {group_name} no tiene participantes en la respuesta de la API")
 
                     for participant in participants:
                         whatsapp_web_id = participant.get('id', {}).get('_serialized')
@@ -247,8 +254,15 @@ class WWGroup(models.Model):
                     # Actualizar contactos del grupo
                     group.write({'contact_ids': [(6, 0, contact_ids)]})
                     
-                    # Update discussion channel members
-                    group._update_discussion_channel()
+                    # Update discussion channel members solo si hay contactos
+                    if contact_ids:
+                        # Si es un grupo nuevo sin canal, crear uno
+                        if not group.channel_id:
+                            group._create_discussion_channel()
+                        else:
+                            group._update_discussion_channel()
+                    else:
+                        _logger.info(f"Grupo {group_name} sincronizado sin contactos - no se creará canal de discusión")
 
                     # Process messages if available
                     messages = group_data.get('messages', [])
@@ -259,4 +273,59 @@ class WWGroup(models.Model):
                 _logger.error("Error en la sincronización de grupos para la cuenta %s: %s", account.name, str(e))
                 continue
 
-        return True 
+        return True
+    
+    def send_whatsapp_message(self, body, attachment=None, wa_template_id=None):
+        """Enviar mensaje WhatsApp al grupo"""
+        self.ensure_one()
+        
+        if not self.whatsapp_account_id:
+            raise ValueError("Group must have a WhatsApp account configured")
+        
+        # Crear mail.message si hay adjunto
+        mail_message = None
+        if attachment:
+            mail_message = self.env['mail.message'].create({
+                'body': body,
+                'attachment_ids': [(4, attachment.id)]
+            })
+        
+        # Crear mensaje WhatsApp
+        message_vals = {
+            'body': body,
+            'recipient_type': 'group',
+            'whatsapp_group_id': self.id,
+            'mobile_number': self.whatsapp_web_id,
+            'wa_account_id': self.whatsapp_account_id.id,
+            'state': 'outgoing',
+        }
+        
+        if mail_message:
+            message_vals['mail_message_id'] = mail_message.id
+            
+        if wa_template_id:
+            message_vals['wa_template_id'] = wa_template_id
+        
+        whatsapp_message = self.env['whatsapp.message'].create(message_vals)
+        
+        # Enviar mensaje
+        whatsapp_message._send_message()
+        
+        return whatsapp_message
+    
+    def action_send_whatsapp_message(self):
+        """Acción para abrir el composer de WhatsApp para el grupo"""
+        self.ensure_one()
+        
+        return {
+            'type': 'ir.actions.act_window',
+            'name': f'Send Message to {self.name}',
+            'res_model': 'whatsapp.composer',
+            'view_mode': 'form',
+            'target': 'new',
+            'context': {
+                'default_recipient_type': 'group',
+                'default_whatsapp_group_id': self.id,
+                'default_wa_template_id': False,
+            }
+        } 

+ 19 - 0
views/marketing_activity_views.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <!-- Vista heredada que agrega el campo de grupo de WhatsApp después del template -->
+    <record id="marketing_activity_view_form_inherit_group" model="ir.ui.view">
+        <field name="name">marketing.activity.form.inherit.group</field>
+        <field name="model">marketing.activity</field>
+        <field name="inherit_id" ref="marketing_automation_whatsapp.marketing_activity_view_form"/>
+        <field name="priority">5</field>
+        <field name="arch" type="xml">
+            <!-- Agregar el campo de grupo de WhatsApp después del campo whatsapp_template_id -->
+            <xpath expr="//field[@name='whatsapp_template_id']" position="after">
+                <field name="whatsapp_group_id" 
+                       invisible="activity_type != 'whatsapp'" 
+                       placeholder="Seleccionar grupo de WhatsApp (opcional)..."
+                       help="Si se selecciona un grupo, todos los mensajes se enviarán al grupo. Si está vacío, se envían a destinatarios individuales según la plantilla."/>
+            </xpath>
+        </field>
+    </record>
+</odoo>

+ 19 - 11
views/ww_group_views.xml

@@ -20,11 +20,19 @@
             <field name="model">ww.group</field>
             <field name="arch" type="xml">
                 <form string="Grupo WhatsApp">
+                    <header>
+                        <button name="action_send_whatsapp_message" 
+                                string="Send WhatsApp Message" 
+                                type="object" 
+                                class="btn-primary"
+                                invisible="not whatsapp_account_id"/>
+                    </header>
                     <sheet>
                         <group>
                             <field name="name"/>
                             <field name="whatsapp_web_id"/>
                             <field name="whatsapp_account_id"/>
+                            <field name="channel_id" readonly="1"/>
                         </group>
                         <notebook>
                             <page string="Contactos">
@@ -36,27 +44,27 @@
             </field>
         </record>
 
-        <!-- Acción de ventana -->
+        <!-- Acción -->
         <record id="action_ww_group" model="ir.actions.act_window">
-            <field name="name">Grupos WhatsApp</field>
+            <field name="name">Grupos de WhatsApp</field>
             <field name="res_model">ww.group</field>
             <field name="view_mode">list,form</field>
+            <field name="view_id" ref="view_ww_group_list"/>
             <field name="help" type="html">
                 <p class="o_view_nocontent_smiling_face">
-                    No hay grupos de WhatsApp
+                    Crear un nuevo grupo de WhatsApp
+                </p>
+                <p>
+                    Los grupos de WhatsApp te permiten enviar mensajes a múltiples contactos de forma simultánea.
                 </p>
             </field>
         </record>
 
-        <!-- Menú -->
-        <menuitem id="menu_whatsapp_web_root"
-                  name="WhatsApp Web"
-                  sequence="10"/>
-
+        <!-- Menú Grupos dentro del menú nativo de WhatsApp -->
         <menuitem id="menu_whatsapp_web_groups"
                   name="Grupos"
-                  parent="menu_whatsapp_web_root"
+                  parent="whatsapp.whatsapp_menu_main"
                   action="action_ww_group"
-                  sequence="10"/>
+                  sequence="3"/>
     </data>
-</odoo> 
+</odoo>