from odoo import models, fields, api from odoo.tools import groupby from odoo.exceptions import ValidationError import logging import markupsafe import requests import json import time import random import re import html import base64 _logger = logging.getLogger(__name__) class WhatsAppMessage(models.Model): _inherit = 'whatsapp.message' # Campos para soporte de grupos recipient_type = fields.Selection([ ('phone', 'Phone Number'), ('group', 'WhatsApp Group') ], string='Recipient Type', default='phone', help="Type of recipient: phone number or WhatsApp group") 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""" for record in self: if record.recipient_type == 'group' and record.whatsapp_group_id: record.final_recipient = record.whatsapp_group_id.whatsapp_web_id else: record.final_recipient = record.mobile_number final_recipient = fields.Char('Final Recipient', compute='_compute_final_recipient', help="Final recipient (phone or group ID)") @api.depends('mobile_number', 'recipient_type') def _compute_mobile_number_formatted(self): """Override SOLO para casos específicos de grupos con WhatsApp Web""" for message in self: # SOLO intervenir si es grupo CON WhatsApp Web configurado if (hasattr(message, 'recipient_type') and message.recipient_type == 'group' and message.wa_account_id and message.wa_account_id.whatsapp_web_url and message.mobile_number and message.mobile_number.endswith('@g.us')): message.mobile_number_formatted = message.mobile_number else: # TODOS LOS DEMÁS CASOS: usar lógica original sin modificar super(WhatsAppMessage, message)._compute_mobile_number_formatted() @api.constrains('recipient_type', 'mobile_number', 'whatsapp_group_id') def _check_recipient_configuration(self): """Validar configuración de destinatario""" 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)") elif record.recipient_type == 'phone': if not record.mobile_number or record.mobile_number.endswith('@g.us'): raise ValidationError("Para mensajes a teléfonos, debe proporcionar un número telefónico válido") @api.onchange('recipient_type') def _onchange_recipient_type(self): """Limpiar campos al cambiar tipo de destinatario""" if self.recipient_type == 'group': self.mobile_number = False else: self.whatsapp_group_id = False def _whatsapp_phone_format(self, fpath=None, number=None, raise_on_format_error=False): """Override SOLO para casos específicos de grupos - NO interferir con funcionalidad nativa""" self.ensure_one() # SOLO intervenir en casos muy específicos de grupos # Si es un mensaje a grupo Y tiene WhatsApp Web configurado if (hasattr(self, 'recipient_type') and self.recipient_type == 'group' and self.wa_account_id and self.wa_account_id.whatsapp_web_url): if self.whatsapp_group_id: return self.whatsapp_group_id.whatsapp_web_id # Si el número es un ID de grupo (termina en @g.us), retornarlo sin validar elif number and number.endswith('@g.us'): return number elif self.mobile_number and self.mobile_number.endswith('@g.us'): return self.mobile_number # TODOS LOS DEMÁS CASOS: usar validación original sin modificar return super()._whatsapp_phone_format(fpath, number, raise_on_format_error) def _get_final_destination(self): """Método mejorado para obtener destino final (grupo o teléfono)""" self.ensure_one() # 1. Si es tipo grupo y hay grupo seleccionado if self.recipient_type == 'group' and self.whatsapp_group_id: try: return self.whatsapp_group_id.whatsapp_web_id except Exception: # Si el modelo ww.group no existe, usar mobile_number pass # 2. Si el mobile_number es un ID de grupo if self.mobile_number and self.mobile_number.endswith('@g.us'): return self.mobile_number return False def _send_message(self, with_commit=False): url = '' if self.wa_account_id and self.wa_account_id.whatsapp_web_url: url = self.wa_account_id.whatsapp_web_url _logger.info('WHATSAPP WEB SEND MESSAGE' + url) group = '' if not url: super()._send_message(with_commit) for whatsapp_message in self: # Determinar destinatario final usando solo la nueva lógica final_destination = whatsapp_message._get_final_destination() if final_destination: group = final_destination attachment = False if whatsapp_message.wa_template_id: record = self.env[whatsapp_message.wa_template_id.model].browse(whatsapp_message.mail_message_id.res_id) #codigo con base a whatsapp.message y whatsapp.template para generacion de adjuntos RecordModel = self.env[whatsapp_message.mail_message_id.model].with_user(whatsapp_message.create_uid) from_record = RecordModel.browse(whatsapp_message.mail_message_id.res_id) # if retrying message then we need to unlink previous attachment # in case of header with report in order to generate it again if whatsapp_message.wa_template_id.report_id and whatsapp_message.wa_template_id.header_type == 'document' and whatsapp_message.mail_message_id.attachment_ids: whatsapp_message.mail_message_id.attachment_ids.unlink() if not attachment and whatsapp_message.wa_template_id.report_id: attachment = whatsapp_message.wa_template_id._generate_attachment_from_report(record) if not attachment and whatsapp_message.wa_template_id.header_attachment_ids: attachment = whatsapp_message.wa_template_id.header_attachment_ids[0] if attachment and attachment not in whatsapp_message.mail_message_id.attachment_ids: whatsapp_message.mail_message_id.attachment_ids = [(4, attachment.id)] # no template elif whatsapp_message.mail_message_id.attachment_ids: attachment = whatsapp_message.mail_message_id.attachment_ids[0] #codigo para limpiar body y numero body = whatsapp_message.body # Asegurar que body sea string y limpiar HTML if body: if isinstance(body, markupsafe.Markup): text = html.unescape(str(body)) else: text = str(body) # Reemplazamos las etiquetas BR y P text = re.sub(r'|', '\n', text) text = re.sub(r'

|

', '\n\n', text) text = re.sub(r'

|

', '', text) # Eliminamos el resto de etiquetas HTML text = re.sub(r'<[^>]+>', '', text) # Limpiamos múltiples saltos de línea text = re.sub(r'\n\s*\n\s*\n', '\n\n', text) # Limpiamos espacios en blanco al inicio y final body = text.strip() # Asegurar que no esté vacío if not body: body = "Mensaje de WhatsApp" else: body = "Mensaje de WhatsApp" # Determinar número/destinatario final if group: # Si ya hay un grupo determinado, usarlo number = group else: # Formatear número según el tipo de destinatario if whatsapp_message.recipient_type == 'group': if whatsapp_message.whatsapp_group_id: number = whatsapp_message.whatsapp_group_id.whatsapp_web_id elif whatsapp_message.mobile_number and whatsapp_message.mobile_number.endswith('@g.us'): number = whatsapp_message.mobile_number else: _logger.error("Mensaje configurado como grupo pero sin destinatario válido") continue else: # Lógica original para números de teléfono number = whatsapp_message.mobile_number if number: number = number.replace(' ', '').replace('+','').replace('-','') if number.startswith("52") and len(number) == 12: number = "521" + number[2:] if len(number) == 10: number = "521" + number number = number + '@c.us' # ENVIO DE MENSAJE # Headers de la petición, si es necesario headers = { "Content-Type": "application/json" } #$wa::sendMessage("521{$fields_data[$settings['borax_whatsapp_mobile']]}@c.us", ['type' => 'MessageMedia', 'args' => [mime_content_type($file), base64_encode(file_get_contents($file)), $filename, $filesize]], ['caption' => $borax_whatsapp_mensaje]); parent_message_id = '' if whatsapp_message.mail_message_id and whatsapp_message.mail_message_id.parent_id: parent_id = whatsapp_message.mail_message_id.parent_id.wa_message_ids if parent_id: parent_message_id = parent_id[0].msg_uid # Validar que tenemos un destinatario válido if not number: _logger.error("No se pudo determinar el destinatario para el mensaje") continue # Validar que tenemos un cuerpo de mensaje válido if not body or not isinstance(body, str): _logger.error("Cuerpo del mensaje inválido: %s", body) body = "Mensaje de WhatsApp" # Log del payload para debugging _logger.info("Enviando mensaje a %s: %s", number, body[:50] + "..." if len(body) > 50 else body) if attachment: payload = { "method": "sendMessage", "args": [number, {'type': 'MessageMedia', 'args': [attachment.mimetype, base64.b64encode(attachment.raw).decode('utf-8'), attachment.name, attachment.file_size]}, {'caption': body}] } else: payload = { "method": "sendMessage", "args": [number, body, {}] } if parent_message_id: payload['args'][2]['quotedMessageId'] = parent_message_id # Realizando la petición POST response = requests.post(url, data=json.dumps(payload), headers=headers) # Verificando si la respuesta contiene data->id if response.status_code == 200: try: response_json = response.json() except ValueError: _logger.error("La respuesta no es JSON válido: %s", response.text) response_json = {} if "_data" in response_json and "id" in response_json["_data"]: _logger.info(f"Petición exitosa. ID: {response_json['_data']['id']['id']}") whatsapp_message.write({ 'state': 'sent', 'msg_uid': response_json['_data']['id']['_serialized'] }) self._cr.commit() else: _logger.info("La respuesta no contiene 'data->id'.") else: _logger.info(f"Error en la petición. Código de estado: {response.status_code}") time.sleep(random.randint(3, 7))