فهرست منبع

Squashed 'whatsapp_web_groups/' content from commit 3ebe73c

git-subtree-dir: whatsapp_web_groups
git-subtree-split: 3ebe73cd3cacfbfa25fef06cba34fb5b84cca052
odoo 2 ماه پیش
کامیت
1dae9b0ea0

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

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

+ 1 - 0
__init__.py

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

+ 31 - 0
__manifest__.py

@@ -0,0 +1,31 @@
+{
+    'name': 'Gestor de Grupos de WhatsApp',
+    'version': '1.0',
+    'summary': 'Gestión de grupos y contactos de WhatsApp Web',
+    'description': 'Permite gestionar grupos, contactos y roles de WhatsApp Web, sincronizando con la API de whatsapp_web.js',
+    'author': 'MC Team',
+    'category': 'Tools',
+    'depends': [
+        'base',
+        'contacts',
+        'whatsapp_web',
+        'marketing_automation_whatsapp',
+        'mail',
+        'calendar',
+        'helpdesk',
+    ],
+    'data': [
+        'security/ir.model.access.csv',
+        'views/ww_contact_views.xml',
+        'views/ww_group_views.xml',
+        'views/ww_role_views.xml',
+        'views/ww_group_contact_rel_views.xml',
+        'views/marketing_activity_views.xml',
+        'views/whatsapp_message_views.xml',
+        'views/whatsapp_composer_views.xml',
+        'data/ir_cron.xml',
+    ],
+    'installable': True,
+    'application': True,
+    'auto_install': False,
+} 

+ 14 - 0
data/ir_cron.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo noupdate="1">
+
+    <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>
+    
+</odoo> 

+ 7 - 0
models/__init__.py

@@ -0,0 +1,7 @@
+from . import ww_contact
+from . import ww_group
+from . import ww_role
+from . import ww_group_contact_rel
+from . import marketing_activity
+from . import whatsapp_message
+from . import whatsapp_composer 

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

+ 115 - 0
models/whatsapp_composer.py

@@ -0,0 +1,115 @@
+from odoo import models, fields, api
+from odoo.exceptions import ValidationError
+import logging
+
+_logger = logging.getLogger(__name__)
+
+class WhatsAppComposer(models.TransientModel):
+    _inherit = 'whatsapp.composer'
+
+    # Campo Many2one para grupos - solo disponible cuando whatsapp_web_groups está instalado
+    whatsapp_group_id = fields.Many2one('ww.group', string='WhatsApp Group', 
+                                        help="Select WhatsApp group to send message to",
+                                        ondelete='set null')
+
+    @api.onchange('whatsapp_group_id')
+    def _onchange_whatsapp_group_id(self):
+        """Actualizar campos cuando se selecciona un grupo"""
+        if self.whatsapp_group_id:
+            self.whatsapp_group_id_char = self.whatsapp_group_id.whatsapp_web_id
+            self.recipient_type = 'group'
+            self.phone = False
+
+    @api.onchange('recipient_type')
+    def _onchange_recipient_type(self):
+        """Limpiar campos al cambiar tipo de destinatario"""
+        super()._onchange_recipient_type()
+        if self.recipient_type != 'group':
+            self.whatsapp_group_id = False
+
+    @api.constrains('recipient_type', 'phone', 'whatsapp_group_id', 'whatsapp_group_id_char', 'wa_template_id', 'body')
+    def _check_recipient_configuration(self):
+        """Extender validación para incluir whatsapp_group_id"""
+        super()._check_recipient_configuration()
+        
+        for record in self:
+            if record.recipient_type == 'group':
+                if not record.whatsapp_group_id and not record.whatsapp_group_id_char:
+                    raise ValidationError("Please select a WhatsApp group or enter a Group ID when sending to groups")
+
+    def _send_whatsapp_web_message(self):
+        """Extender método para usar whatsapp_group_id si está disponible"""
+        records = self._get_active_records()
+        
+        for record in records:
+            # Determinar destinatario - priorizar whatsapp_group_id sobre whatsapp_group_id_char
+            if self.recipient_type == 'group':
+                if self.whatsapp_group_id:
+                    mobile_number = self.whatsapp_group_id.whatsapp_web_id
+                elif self.whatsapp_group_id_char:
+                    mobile_number = self.whatsapp_group_id_char
+                else:
+                    raise ValidationError("Please specify a group")
+            else:
+                mobile_number = self.phone
+                if not mobile_number:
+                    raise ValidationError("Please provide a phone number")
+            
+            # Crear mail.message con adjuntos si existen
+            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
+            message_vals = {
+                'mail_message_id': message.id,
+                'mobile_number': mobile_number,
+                'mobile_number_formatted': mobile_number,
+                'recipient_type': self.recipient_type,
+                'wa_template_id': False,
+                'wa_account_id': self._get_whatsapp_web_account().id,
+                'state': 'outgoing',
+            }
+            
+            # Agregar whatsapp_group_id si está disponible
+            if self.whatsapp_group_id:
+                message_vals['whatsapp_group_id'] = self.whatsapp_group_id.id
+            
+            whatsapp_message = self.env['whatsapp.message'].create(message_vals)
+            
+            # Enviar mensaje
+            whatsapp_message._send_message()
+        
+        return {'type': 'ir.actions.act_window_close'}
+
+    def _prepare_whatsapp_message_values(self, record):
+        """Extender método para agregar información de grupo"""
+        values = super()._prepare_whatsapp_message_values(record)
+        
+        # Agregar información de grupo si está disponible
+        if (hasattr(self, 'recipient_type') and self.recipient_type == 'group'):
+            if self.whatsapp_group_id:
+                values.update({
+                    'whatsapp_group_id': self.whatsapp_group_id.id,
+                    'mobile_number': self.whatsapp_group_id.whatsapp_web_id,
+                    'mobile_number_formatted': self.whatsapp_group_id.whatsapp_web_id,
+                })
+            elif self.whatsapp_group_id_char:
+                values.update({
+                    'mobile_number': self.whatsapp_group_id_char,
+                    'mobile_number_formatted': self.whatsapp_group_id_char,
+                })
+        
+        return values
+

+ 64 - 0
models/whatsapp_message.py

@@ -0,0 +1,64 @@
+from odoo import models, fields, api
+from odoo.exceptions import ValidationError
+import logging
+
+_logger = logging.getLogger(__name__)
+
+class WhatsAppMessage(models.Model):
+    _inherit = 'whatsapp.message'
+
+    # Campo Many2one para grupos - solo disponible cuando whatsapp_web_groups está instalado
+    whatsapp_group_id = fields.Many2one('ww.group', string='WhatsApp Group', 
+                                        help="WhatsApp group to send message to (if recipient_type is group)",
+                                        ondelete='set null')
+
+    @api.depends('recipient_type', 'mobile_number', 'whatsapp_group_id')
+    def _compute_final_recipient(self):
+        """Compute the final recipient based on type - extiende la lógica base"""
+        # Primero ejecutar la lógica base de whatsapp_web
+        super()._compute_final_recipient()
+        
+        # Si hay grupo seleccionado, usar su ID (sobrescribe la lógica base)
+        for record in self:
+            if record.recipient_type == 'group' and record.whatsapp_group_id:
+                record.final_recipient = record.whatsapp_group_id.whatsapp_web_id
+
+    @api.onchange('whatsapp_group_id')
+    def _onchange_whatsapp_group_id(self):
+        """Actualizar mobile_number cuando se selecciona un grupo"""
+        if self.whatsapp_group_id:
+            self.mobile_number = self.whatsapp_group_id.whatsapp_web_id
+            self.recipient_type = 'group'
+
+    @api.constrains('recipient_type', 'mobile_number', 'whatsapp_group_id')
+    def _check_recipient_configuration(self):
+        """Extender validación para incluir whatsapp_group_id"""
+        super()._check_recipient_configuration()
+        
+        for record in self:
+            if record.recipient_type == 'group':
+                if not record.whatsapp_group_id and not (record.mobile_number and record.mobile_number.endswith('@g.us')):
+                    raise ValidationError("Para mensajes a grupos, debe seleccionar un grupo o proporcionar un ID de grupo válido (@g.us)")
+
+    def _get_final_destination(self):
+        """Método mejorado para obtener destino final - extiende la lógica base"""
+        self.ensure_one()
+        
+        # Si hay grupo seleccionado, usar su ID
+        if self.recipient_type == 'group' and self.whatsapp_group_id:
+            return self.whatsapp_group_id.whatsapp_web_id
+        
+        # De lo contrario, usar la lógica base (incluye verificación de mobile_number @g.us)
+        result = super()._get_final_destination()
+        if result:
+            return result
+        
+        # Fallback adicional si no hay resultado
+        return False
+
+    def _send_message(self, with_commit=False):
+        """Extender método _send_message para manejar whatsapp_group_id"""
+        # El método _get_final_destination ya maneja whatsapp_group_id,
+        # así que la lógica base funcionará correctamente
+        return super()._send_message(with_commit)
+

+ 37 - 0
models/ww_contact.py

@@ -0,0 +1,37 @@
+from odoo import models, fields
+
+class WWContact(models.Model):
+    _inherit = 'res.partner'
+    _description = 'Contacto de WhatsApp Web'
+
+    whatsapp_web_id = fields.Char(string='ID WhatsApp Web', index=True, help='ID único del contacto en WhatsApp Web')
+    group_ids = fields.Many2many(
+        comodel_name='ww.group',
+        relation='ww_group_contact_rel',
+        column1='contact_id',
+        column2='group_id',
+        string='Grupos',
+        readonly=True,
+    )
+    channel_ids = fields.Many2many(
+        comodel_name='discuss.channel',
+        relation='discuss_channel_member',
+        column1='partner_id',
+        column2='channel_id',
+        string='Canales',
+        readonly=True,
+    )
+    meeting_ids = fields.One2many(
+        comodel_name='calendar.event',
+        inverse_name='partner_id',
+        string='Reuniones',
+        readonly=True,
+    )
+    sla_ids = fields.Many2many(
+        comodel_name='helpdesk.sla',
+        relation='helpdesk_sla_partner',
+        column1='partner_id',
+        column2='sla_id',
+        string='SLAs',
+        readonly=True,
+    ) 

+ 331 - 0
models/ww_group.py

@@ -0,0 +1,331 @@
+from odoo import models, fields, api
+import logging
+from datetime import datetime
+
+_logger = logging.getLogger(__name__)
+
+class WWGroup(models.Model):
+    _name = 'ww.group'
+    _description = 'Grupo de WhatsApp Web'
+
+    name = fields.Char(string='Nombre del Grupo', required=True)
+    whatsapp_web_id = fields.Char(string='ID WhatsApp Web', index=True, help='ID único del grupo en WhatsApp Web')
+    whatsapp_account_id = fields.Many2one('whatsapp.account', string='Cuenta de WhatsApp', required=True)
+    channel_id = fields.Many2one('discuss.channel', string='Canal de Discusión', readonly=True)
+    contact_ids = fields.Many2many(
+        comodel_name='res.partner',
+        relation='ww_group_contact_rel',
+        column1='group_id',
+        column2='contact_id',
+        string='Contactos',
+        readonly=True,
+    )
+
+    def _process_messages(self, messages_data):
+        """Process WhatsApp messages and create them in the channel"""
+        self.ensure_one()
+        
+        if not messages_data or not self.channel_id:
+            return True
+
+        # Get existing message IDs to avoid duplicates
+        existing_ids = set(self.channel_id.message_ids.mapped('message_id'))
+        
+        # Prepare bulk create values
+        message_vals_list = []
+        for msg_data in messages_data:
+            msg_id = msg_data.get('id', {}).get('_serialized')
+            
+            # Skip if message already exists
+            if msg_id in existing_ids:
+                continue
+
+            # Get author partner
+            author_whatsapp_id = msg_data.get('author')
+            author = self.env['res.partner'].search([
+                ('whatsapp_web_id', '=', author_whatsapp_id)
+            ], limit=1) if author_whatsapp_id else False
+
+            # Get quoted message author if exists
+            quoted_author = False
+            if msg_data.get('hasQuotedMsg') and msg_data.get('quotedParticipant'):
+                quoted_author = self.env['res.partner'].search([
+                    ('whatsapp_web_id', '=', msg_data['quotedParticipant'])
+                ], limit=1)
+
+            # Convert timestamp to datetime
+            timestamp = datetime.fromtimestamp(msg_data.get('timestamp', 0))
+
+            # Prepare message body with author and content
+            author_name = author.name if author else "Desconocido"
+            message_body = f"{msg_data.get('body', '')}"
+
+            # Add quoted message if exists
+            if msg_data.get('hasQuotedMsg') and msg_data.get('quotedMsg', {}).get('body'):
+                quoted_author_name = quoted_author.name if quoted_author else "Desconocido"
+                message_body += f"\n\n<blockquote><strong>{quoted_author_name}:</strong> {msg_data['quotedMsg']['body']}</blockquote>"
+
+            message_vals = {
+                'model': 'discuss.channel',
+                'res_id': self.channel_id.id,
+                'message_type': 'comment',
+                'subtype_id': self.env.ref('mail.mt_comment').id,
+                'body': message_body,
+                'date': timestamp,
+                'author_id': author.id if author else self.env.user.partner_id.id,
+                'message_id': msg_id,
+            }
+            message_vals_list.append(message_vals)
+
+        # Bulk create messages
+        if message_vals_list:
+            self.env['mail.message'].create(message_vals_list)
+
+        return True
+
+    def _create_discussion_channel(self):
+        """Create a discussion channel for the WhatsApp group"""
+        self.ensure_one()
+        
+        try:
+            # Verificar si ya existe un canal para este grupo
+            if self.channel_id:
+                return self.channel_id
+
+            # Create channel name with WhatsApp prefix
+            channel_name = f"📱 {self.name}"
+            
+            # Verificar que hay contactos
+            if not self.contact_ids:
+                _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
+            partner_ids = []
+            for contact in self.contact_ids:
+                if contact and contact.id:
+                    partner_ids.append(contact.id)
+
+            if not partner_ids:
+                _logger.warning(f"No se encontraron IDs válidos de contactos para el grupo {self.name}")
+                return False
+
+            # Create the channel using channel_create
+            channel = self.env['discuss.channel'].channel_create(
+                name=channel_name,
+                group_id=self.env.user.groups_id[0].id,  # Usar el primer grupo del usuario actual
+            )
+            
+            # Add members to the channel
+            channel.add_members(partner_ids=partner_ids)
+            
+            # Link the channel to the group
+            self.write({'channel_id': channel.id})
+            return channel
+            
+        except Exception as e:
+            _logger.error(f"Error al crear el canal para el grupo {self.name}: {str(e)}")
+            return False
+
+    def _update_discussion_channel(self):
+        """Update the discussion channel members"""
+        self.ensure_one()
+        
+        try:
+            # Si no existe el canal, intentar crearlo
+            if not self.channel_id:
+                return self._create_discussion_channel()
+            
+            # Verificar que el canal aún existe
+            channel = self.env['discuss.channel'].browse(self.channel_id.id)
+            if not channel.exists():
+                _logger.warning(f"El canal para el grupo {self.name} ya no existe, creando uno nuevo")
+                self.write({'channel_id': False})
+                return self._create_discussion_channel()
+                
+            # Obtener los IDs de los contactos de forma segura
+            partner_ids = []
+            for contact in self.contact_ids:
+                if contact and contact.id:
+                    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} - saltando actualización")
+                # Si no hay contactos, no actualizar pero no fallar
+                return channel
+
+            # Update channel members using add_members
+            channel.add_members(partner_ids=partner_ids)
+            return channel
+            
+        except Exception as e:
+            _logger.error(f"Error al actualizar el canal para el grupo {self.name}: {str(e)}")
+            return False
+
+    @api.model
+    def sync_ww_contacts_groups(self):
+        """
+        Sincroniza los contactos y grupos de WhatsApp Web.
+        Solo sincroniza contactos que están dentro de grupos y valida que no se dupliquen,
+        verificando los últimos 10 dígitos del campo mobile.
+        """
+        accounts = self.env['whatsapp.account'].search([])
+        
+        for account in accounts:
+            try:
+                # Obtener grupos usando el método de la cuenta
+                groups_data = account.get_groups()
+                if not groups_data:
+                    continue
+                
+                # Procesar cada grupo
+                for group_data in groups_data:
+                    group_id = group_data.get('id').get('_serialized')
+                    group_name = group_data.get('name', 'Sin nombre')
+                    
+                    # Buscar o crear grupo
+                    group = self.search([
+                        ('whatsapp_web_id', '=', group_id),
+                        ('whatsapp_account_id', '=', account.id)
+                    ], limit=1)
+                    
+                    if not group:
+                        group = self.create({
+                            'name': group_name,
+                            'whatsapp_web_id': group_id,
+                            'whatsapp_account_id': account.id
+                        })
+                        # 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:
+                            group.write({'name': group_name})
+                            # Actualizar nombre del canal si existe
+                            if group.channel_id:
+                                group.channel_id.write({'name': f"📱 {group_name}"})
+
+                    # 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')
+                        mobile = participant.get('number', '')
+                        is_admin = participant.get('isAdmin', False)
+                        is_super_admin = participant.get('isSuperAdmin', False)
+
+                        # Derive participant name
+                        participant_name = participant.get('name') or participant.get('pushname') or mobile
+
+                        # Search for existing contact
+                        contact = self.env['res.partner'].search([
+                            ('whatsapp_web_id', '=', whatsapp_web_id)
+                        ], limit=1)
+
+                        if not contact and mobile and len(mobile) >= 10:
+                            last_10_digits = mobile[-10:]
+                            contact = self.env['res.partner'].search([
+                                ('mobile', 'like', '%' + last_10_digits)
+                            ], limit=1)
+
+                        partner_vals = {
+                            'name': participant_name,
+                            'mobile': mobile,
+                            'whatsapp_web_id': whatsapp_web_id,
+                        }
+
+                        if contact:
+                            # Update existing contact
+                            contact.write(partner_vals)
+                        else:
+                            # Create new contact
+                            contact = self.env['res.partner'].create(partner_vals)
+                        
+                        if contact:
+                            contact_ids.append(contact.id)
+                    
+                    # Actualizar contactos del grupo
+                    group.write({'contact_ids': [(6, 0, contact_ids)]})
+                    
+                    # 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', [])
+                    if messages:
+                        group._process_messages(messages)
+
+            except Exception as e:
+                _logger.error("Error en la sincronización de grupos para la cuenta %s: %s", account.name, str(e))
+                continue
+
+        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,
+            }
+        } 

+ 22 - 0
models/ww_group_contact_rel.py

@@ -0,0 +1,22 @@
+from odoo import models, fields
+
+class WWGroupContactRel(models.Model):
+    _name = 'ww.group_contact_rel'
+    _description = 'Relación Contacto-Grupo de WhatsApp Web'
+    _table = 'ww_group_contact_rel'
+
+    # Explicitly define 'id' field. models.Model does this automatically,
+    # but being explicit can sometimes help clarify intent or resolve
+    # obscure schema generation issues if the table had a troubled history.
+    # fields.Id() is the standard way to define Odoo's automatic ID.
+    # id = fields.Id(string='ID')
+
+    group_id = fields.Many2one('ww.group', string='Grupo', required=True, ondelete='cascade', index=True)
+    contact_id = fields.Many2one('res.partner', string='Contacto', required=True, ondelete='cascade', index=True)
+    is_admin = fields.Boolean(string='Administrador del Grupo', default=False)
+    is_super_admin = fields.Boolean(string='Super Administrador del Grupo', default=False)
+    role_id = fields.Many2one('ww.role', string='Rol en el Grupo') 
+
+    _sql_constraints = [
+        ('group_contact_uniq', 'unique(group_id, contact_id)', 'El contacto debe ser único por grupo.')
+    ]

+ 8 - 0
models/ww_role.py

@@ -0,0 +1,8 @@
+from odoo import models, fields
+
+class WWRole(models.Model):
+    _name = 'ww.role'
+    _description = 'Rol de Grupo de WhatsApp Web'
+
+    name = fields.Char(string='Nombre del Rol', required=True)
+    description = fields.Text(string='Descripción') 

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

@@ -0,0 +1,7 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+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
+access_ww_role_user,ww.role.user,model_ww_role,base.group_user,1,1,1,0
+access_ww_role_manager,ww.role.manager,model_ww_role,base.group_system,1,1,1,1
+access_ww_group_contact_rel_user,ww.group.contact.rel.user,model_ww_group_contact_rel,base.group_user,1,1,1,0
+access_ww_group_contact_rel_manager,ww.group.contact.rel.manager,model_ww_group_contact_rel,base.group_system,1,1,1,1

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

+ 22 - 0
views/whatsapp_composer_views.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Extender vista de formulario del composer de WhatsApp para agregar campo Many2one -->
+        <record id="whatsapp_composer_view_form_groups_m2o" model="ir.ui.view">
+            <field name="name">whatsapp.composer.view.form.groups.m2o</field>
+            <field name="model">whatsapp.composer</field>
+            <field name="inherit_id" ref="whatsapp_web.whatsapp_composer_view_form_groups"/>
+            <field name="arch" type="xml">
+                <!-- Agregar campo Many2one whatsapp_group_id antes de whatsapp_group_id_char -->
+                <xpath expr="//field[@name='whatsapp_group_id_char']" position="before">
+                    <field name="whatsapp_group_id" 
+                           invisible="recipient_type != 'group'"
+                           string="WhatsApp Group"
+                           placeholder="Select a WhatsApp group..."
+                           options="{'no_create': True}"/>
+                </xpath>
+            </field>
+        </record>
+    </data>
+</odoo>
+

+ 34 - 0
views/whatsapp_message_views.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Extender vista de formulario de WhatsApp Message para agregar campo Many2one -->
+        <record id="whatsapp_message_view_form_groups_m2o" model="ir.ui.view">
+            <field name="name">whatsapp.message.view.form.groups.m2o</field>
+            <field name="model">whatsapp.message</field>
+            <field name="inherit_id" ref="whatsapp_web.whatsapp_message_view_form_groups"/>
+            <field name="arch" type="xml">
+                <!-- Agregar campo Many2one whatsapp_group_id después de recipient_type -->
+                <xpath expr="//field[@name='recipient_type']" position="after">
+                    <field name="whatsapp_group_id" 
+                           invisible="recipient_type != 'group'"
+                           required="recipient_type == 'group'"
+                           options="{'no_create': True}"/>
+                </xpath>
+            </field>
+        </record>
+
+        <!-- Extender vista de lista de WhatsApp Message -->
+        <record id="whatsapp_message_view_tree_groups_m2o" model="ir.ui.view">
+            <field name="name">whatsapp.message.view.tree.groups.m2o</field>
+            <field name="model">whatsapp.message</field>
+            <field name="inherit_id" ref="whatsapp_web.whatsapp_message_view_tree_groups"/>
+            <field name="arch" type="xml">
+                <!-- Agregar columna whatsapp_group_id -->
+                <xpath expr="//field[@name='recipient_type']" position="after">
+                    <field name="whatsapp_group_id" optional="hide"/>
+                </xpath>
+            </field>
+        </record>
+    </data>
+</odoo>
+

+ 63 - 0
views/ww_contact_views.xml

@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <record id="view_res_partner_whatsapp_tree" model="ir.ui.view">
+            <field name="name">res.partner.whatsapp.tree</field>
+            <field name="model">res.partner</field>
+            <field name="inherit_id" ref="base.view_partner_tree"/>
+            <field name="arch" type="xml">
+                <xpath expr="//field[@name='complete_name']" position="after">
+                    <field name="whatsapp_web_id"/>
+                </xpath>
+            </field>
+        </record>
+
+        <record id="view_res_partner_whatsapp_form" model="ir.ui.view">
+            <field name="name">res.partner.whatsapp.form</field>
+            <field name="model">res.partner</field>
+            <field name="inherit_id" ref="base.view_partner_form"/>
+            <field name="arch" type="xml">
+                <xpath expr="//notebook" position="inside">
+                    <page string="WhatsApp" name="whatsapp">
+                        <group>
+                            <field name="whatsapp_web_id"/>
+                        </group>
+                        <notebook>
+                            <page string="Grupos">
+                                <field name="group_ids" widget="many2many_tags">
+                                    <list string="Grupos">
+                                        <field name="name"/>
+                                    </list>
+                                </field>
+                            </page>
+                        </notebook>
+                    </page>
+                </xpath>
+            </field>
+        </record>
+
+        <record id="view_res_partner_whatsapp_search" model="ir.ui.view">
+            <field name="name">res.partner.whatsapp.search</field>
+            <field name="model">res.partner</field>
+            <field name="inherit_id" ref="base.view_res_partner_filter"/>
+            <field name="arch" type="xml">
+                <xpath expr="//filter[@name='type_company']" position="after">
+                    <filter string="WhatsApp" name="whatsapp" domain="[('whatsapp_web_id', '!=', False)]"/>
+                </xpath>
+            </field>
+        </record>
+
+        <record id="action_res_partner_whatsapp" model="ir.actions.act_window">
+            <field name="name">Contactos WhatsApp Web</field>
+            <field name="res_model">res.partner</field>
+            <field name="view_mode">list,form</field>
+            <field name="domain">[('whatsapp_web_id', '!=', False)]</field>
+            <field name="context">{'search_default_whatsapp': 1}</field>
+            <field name="help" type="html">
+                <p class="o_view_nocontent_smiling_face">
+                    No hay contactos de WhatsApp
+                </p>
+            </field>
+        </record>
+    </data>
+</odoo> 

+ 48 - 0
views/ww_group_contact_rel_views.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Vista de lista -->
+        <record id="view_ww_group_contact_rel_list" model="ir.ui.view">
+            <field name="name">ww.group.contact.rel.list</field>
+            <field name="model">ww.group_contact_rel</field>
+            <field name="arch" type="xml">
+                <list string="Contactos de Grupo" editable="bottom">
+                    <field name="group_id"/>
+                    <field name="contact_id"/>
+                    <field name="is_admin"/>
+                    <field name="role_id"/>
+                </list>
+            </field>
+        </record>
+
+        <!-- Vista de formulario -->
+        <record id="view_ww_group_contact_rel_form" model="ir.ui.view">
+            <field name="name">ww.group.contact.rel.form</field>
+            <field name="model">ww.group_contact_rel</field>
+            <field name="arch" type="xml">
+                <form string="Contacto de Grupo">
+                    <sheet>
+                        <group>
+                            <field name="group_id"/>
+                            <field name="contact_id"/>
+                            <field name="is_admin"/>
+                            <field name="role_id"/>
+                        </group>
+                    </sheet>
+                </form>
+            </field>
+        </record>
+
+        <!-- Acción de ventana -->
+        <record id="action_ww_group_contact_rel" model="ir.actions.act_window">
+            <field name="name">Contactos de Grupo</field>
+            <field name="res_model">ww.group_contact_rel</field>
+            <field name="view_mode">list,form</field>
+            <field name="help" type="html">
+                <p class="o_view_nocontent_smiling_face">
+                    No hay contactos en grupos
+                </p>
+            </field>
+        </record>
+    </data>
+</odoo> 

+ 70 - 0
views/ww_group_views.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Vista de lista -->
+        <record id="view_ww_group_list" model="ir.ui.view">
+            <field name="name">ww.group.list</field>
+            <field name="model">ww.group</field>
+            <field name="arch" type="xml">
+                <list string="Grupos WhatsApp" editable="bottom">
+                    <field name="name"/>
+                    <field name="whatsapp_web_id"/>
+                    <field name="whatsapp_account_id"/>
+                </list>
+            </field>
+        </record>
+
+        <!-- Vista de formulario -->
+        <record id="view_ww_group_form" model="ir.ui.view">
+            <field name="name">ww.group.form</field>
+            <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">
+                                <field name="contact_ids" widget="many2many_tags"/>
+                            </page>
+                        </notebook>
+                    </sheet>
+                </form>
+            </field>
+        </record>
+
+        <!-- Acción -->
+        <record id="action_ww_group" model="ir.actions.act_window">
+            <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">
+                    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ú Grupos dentro del menú nativo de WhatsApp -->
+        <menuitem id="menu_whatsapp_web_groups"
+                  name="Grupos"
+                  parent="whatsapp.whatsapp_menu_main"
+                  action="action_ww_group"
+                  sequence="3"/>
+    </data>
+</odoo>

+ 44 - 0
views/ww_role_views.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <data>
+        <!-- Vista de lista -->
+        <record id="view_ww_role_list" model="ir.ui.view">
+            <field name="name">ww.role.list</field>
+            <field name="model">ww.role</field>
+            <field name="arch" type="xml">
+                <list string="Roles de WhatsApp" editable="bottom">
+                    <field name="name"/>
+                    <field name="description"/>
+                </list>
+            </field>
+        </record>
+
+        <!-- Vista de formulario -->
+        <record id="view_ww_role_form" model="ir.ui.view">
+            <field name="name">ww.role.form</field>
+            <field name="model">ww.role</field>
+            <field name="arch" type="xml">
+                <form string="Rol de WhatsApp">
+                    <sheet>
+                        <group>
+                            <field name="name"/>
+                            <field name="description"/>
+                        </group>
+                    </sheet>
+                </form>
+            </field>
+        </record>
+
+        <!-- Acción de ventana -->
+        <record id="action_ww_role" model="ir.actions.act_window">
+            <field name="name">Roles de WhatsApp</field>
+            <field name="res_model">ww.role</field>
+            <field name="view_mode">list,form</field>
+            <field name="help" type="html">
+                <p class="o_view_nocontent_smiling_face">
+                    No hay roles de WhatsApp
+                </p>
+            </field>
+        </record>
+    </data>
+</odoo>