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 básico de grupos (solo por ID string, sin Many2one)
# La funcionalidad completa de grupos con Many2one está en whatsapp_web_groups
recipient_type = fields.Selection([
('phone', 'Phone Number'),
('group', 'WhatsApp Group')
], string='Recipient Type', default='phone', help="Type of recipient: phone number or WhatsApp group")
@api.depends('recipient_type', 'mobile_number')
def _compute_final_recipient(self):
"""Compute the final recipient based on type"""
for record in self:
# Si es grupo y mobile_number termina en @g.us, usarlo directamente
if record.recipient_type == 'group' and record.mobile_number and record.mobile_number.endswith('@g.us'):
record.final_recipient = record.mobile_number
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')
def _check_recipient_configuration(self):
"""Validar configuración de destinatario"""
for record in self:
if record.recipient_type == 'group':
if not (record.mobile_number and record.mobile_number.endswith('@g.us')):
raise ValidationError("Para mensajes a grupos, debe 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")
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):
# Si el número es un ID de grupo (termina en @g.us), retornarlo sin validar
if 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()
# Si el mobile_number es un ID de grupo (termina en @g.us)
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 = ''
session_name = ''
api_key = ''
if self.wa_account_id and self.wa_account_id.whatsapp_web_url:
url = self.wa_account_id.whatsapp_web_url
session_name = self.wa_account_id.whatsapp_web_login or ''
api_key = self.wa_account_id.whatsapp_web_api_key or ''
_logger.info('WHATSAPP WEB SEND MESSAGE - URL: %s, Session: %s', url, session_name)
group = ''
if not url or not session_name or not api_key:
# Si no hay configuración de WhatsApp Web, usar método original
super()._send_message(with_commit)
return
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.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 - Nueva API Gateway 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" # Determinar si es grupo is_group = number.endswith('@g.us') if number else False # Construir URL base base_url = url.rstrip('/') endpoint = 'send-message' # Headers con autenticación headers = { "Content-Type": "application/json", "X-API-Key": api_key } # Preparar payload según tipo de mensaje if attachment: # Determinar endpoint según tipo de archivo mimetype = attachment.mimetype or 'application/octet-stream' if mimetype.startswith('image/'): endpoint = 'send-image' elif mimetype.startswith('video/'): endpoint = 'send-video' elif mimetype.startswith('audio/'): endpoint = 'send-voice' else: endpoint = 'send-file' # Convertir archivo a base64 con prefijo data URI file_base64 = base64.b64encode(attachment.raw).decode('utf-8') base64_with_prefix = f"data:{mimetype};base64,{file_base64}" payload = { "phone": [number], # Array para send-image/send-file "base64": base64_with_prefix, "filename": attachment.name or "file", "caption": body, "isGroup": is_group } if parent_message_id: payload["quotedMessageId"] = parent_message_id else: # Mensaje de texto payload = { "phone": number, # String para send-message "message": body, "isGroup": is_group } if parent_message_id: payload["quotedMessageId"] = parent_message_id # Construir URL completa full_url = f"{base_url}/api/v1/{session_name}/{endpoint}" # Log del payload para debugging _logger.info("Enviando mensaje a %s (%s) usando endpoint %s", number, "grupo" if is_group else "contacto", endpoint) # Realizar petición POST try: response = requests.post(full_url, json=payload, headers=headers, timeout=60) # Procesar respuesta if response.status_code == 200: try: response_json = response.json() # La nueva API puede devolver jobId (mensaje encolado) o id (enviado directamente) if 'jobId' in response_json: # Mensaje encolado - si la API devuelve jobId, significa que el mensaje fue aceptado # y está en proceso de envío, por lo que lo marcamos como 'sent' job_id = response_json.get('jobId') _logger.info("Mensaje aceptado por la API. Job ID: %s - Marcando como enviado", job_id) whatsapp_message.write({ 'state': 'sent', # Marcar como enviado ya que fue aceptado por la API 'msg_uid': job_id }) self._cr.commit() elif 'id' in response_json: # Mensaje enviado directamente msg_id = response_json.get('id') if isinstance(msg_id, dict) and '_serialized' in msg_id: msg_uid = msg_id['_serialized'] elif isinstance(msg_id, str): msg_uid = msg_id else: msg_uid = str(msg_id) _logger.info("Mensaje enviado exitosamente. ID: %s", msg_uid) whatsapp_message.write({ 'state': 'sent', 'msg_uid': msg_uid }) self._cr.commit() else: _logger.warning("Respuesta exitosa pero sin jobId ni id: %s", response_json) whatsapp_message.write({ 'state': 'outgoing' }) self._cr.commit() except ValueError: _logger.error("La respuesta no es JSON válido: %s", response.text) whatsapp_message.write({ 'state': 'error' }) self._cr.commit() else: _logger.error("Error en la petición. Código: %s, Respuesta: %s", response.status_code, response.text) whatsapp_message.write({ 'state': 'error' }) self._cr.commit() except requests.exceptions.RequestException as e: _logger.error("Error de conexión al enviar mensaje: %s", str(e)) whatsapp_message.write({ 'state': 'error' }) self._cr.commit() time.sleep(random.randint(3, 7))