| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- 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}"
- # 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,
- },
- }
|