root 8 månader sedan
förälder
incheckning
df0b1a28fe

+ 1 - 0
__init__.py

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

+ 27 - 0
__manifest__.py

@@ -0,0 +1,27 @@
+{
+    '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',
+        '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',
+        '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> 

+ 4 - 0
models/__init__.py

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

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

+ 262 - 0
models/ww_group.py

@@ -0,0 +1,262 @@
+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}")
+                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}")
+                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
+                        })
+                        # Create discussion channel for new group
+                        group._create_discussion_channel()
+                    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 = []
+
+                    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
+                    group._update_discussion_channel()
+
+                    # 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 

+ 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

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

+ 62 - 0
views/ww_group_views.xml

@@ -0,0 +1,62 @@
+<?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">
+                    <sheet>
+                        <group>
+                            <field name="name"/>
+                            <field name="whatsapp_web_id"/>
+                            <field name="whatsapp_account_id"/>
+                        </group>
+                        <notebook>
+                            <page string="Contactos">
+                                <field name="contact_ids" widget="many2many_tags"/>
+                            </page>
+                        </notebook>
+                    </sheet>
+                </form>
+            </field>
+        </record>
+
+        <!-- Acción de ventana -->
+        <record id="action_ww_group" model="ir.actions.act_window">
+            <field name="name">Grupos WhatsApp</field>
+            <field name="res_model">ww.group</field>
+            <field name="view_mode">list,form</field>
+            <field name="help" type="html">
+                <p class="o_view_nocontent_smiling_face">
+                    No hay grupos de WhatsApp
+                </p>
+            </field>
+        </record>
+
+        <!-- Menú -->
+        <menuitem id="menu_whatsapp_web_root"
+                  name="WhatsApp Web"
+                  sequence="10"/>
+
+        <menuitem id="menu_whatsapp_web_groups"
+                  name="Grupos"
+                  parent="menu_whatsapp_web_root"
+                  action="action_ww_group"
+                  sequence="10"/>
+    </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>