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
{quoted_author_name}: {msg_data['quotedMsg']['body']}" 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}" # Obtener los IDs de los contactos de forma segura partner_ids = [] if self.contact_ids: for contact in self.contact_ids: if contact and contact.id: partner_ids.append(contact.id) # Create the channel using standard create method for maximum robustness _logger.info(f"WWGroup: Creating channel via create() for {self.name}") # Default values for a public channel in WhatsApp section # We use channel_type='whatsapp' so it appears in the correct UI section channel_vals = { "name": channel_name, "channel_type": "whatsapp", "whatsapp_number": self.whatsapp_web_id, # @g.us ID "wa_account_id": self.whatsapp_account_id.id, "group_public_id": None, } channel = self.env["discuss.channel"].create(channel_vals) _logger.info(f"WWGroup: WhatsApp Channel created with ID {channel.id}") # Add members to the channel if any # AUTOMATICALLY ADD system user / account owner so they can see the channel if self.whatsapp_account_id and self.whatsapp_account_id.create_uid: owner_partner = self.whatsapp_account_id.create_uid.partner_id if owner_partner and owner_partner.id not in partner_ids: partner_ids.append(owner_partner.id) if partner_ids: _logger.info( f"WWGroup: Adding {len(partner_ids)} members to channel {channel.id}" ) 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)}", exc_info=True, ) return False except Exception as e: _logger.error( f"Error al crear el canal para el grupo {self.name}: {str(e)}" ) return False def process_webhook_group(self, account, group_metadata, sender_partner=False): """ Process group metadata from webhook to Create/Update group and handle members gracefully. Args: account: whatsapp.account record group_metadata: dict {'id':..., 'name':...} sender_partner: res.partner record (the sender of the message) Returns: ww.group record or False """ try: group_id = group_metadata.get("id") group_name = group_metadata.get("name", "Sin nombre") if not group_id: return False # 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, } ) else: # Actualizar nombre del grupo si cambi贸 y no es el default if group.name != group_name and group_name != "Sin nombre": group.write({"name": group_name}) # Actualizar nombre del canal si existe if group.channel_id: group.channel_id.write({"name": f"馃摫 {group_name}"}) # Organic Member Growth: Add sender to group if not present if sender_partner: if sender_partner.id not in group.contact_ids.ids: _logger.info( f"Adding sender {sender_partner.name} to group {group.name}" ) group.write({"contact_ids": [(4, sender_partner.id)]}) # Ensure channel exists (Lazy creation) if not group.channel_id: group._create_discussion_channel() # Organic Member Growth: Add sender to channel if not present if group.channel_id and sender_partner: # Check membership (this check might be redundant as add_members handles duplication, but safe) group.channel_id.add_members(partner_ids=[sender_partner.id]) return group except Exception as e: _logger.error(f"Error processing webhook group {group_metadata}: {e}") return False def process_group_data(self, account, group_data): """ Process a single group data (from API SYNC): create/update group, sync participants. Returns the group object or False. """ try: 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, } ) 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" ) for participant in participants: whatsapp_web_id = participant.get("id", {}).get("_serialized") mobile = participant.get("number", "") # 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 if not group.channel_id: group._create_discussion_channel() else: group._update_discussion_channel() # Process messages if available messages = group_data.get("messages", []) if messages: group._process_messages(messages) return group except Exception as e: _logger.error( f"Error procesando datos del grupo {group_data.get('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: self.process_group_data(account, group_data) 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, }, }