whatsapp_message.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. from odoo import models, fields, api
  2. from odoo.tools import groupby
  3. from odoo.exceptions import ValidationError
  4. import logging
  5. import markupsafe
  6. import requests
  7. import json
  8. import time
  9. import random
  10. import re
  11. import html
  12. import base64
  13. _logger = logging.getLogger(__name__)
  14. class WhatsAppMessage(models.Model):
  15. _inherit = 'whatsapp.message'
  16. # Campos para soporte de grupos
  17. recipient_type = fields.Selection([
  18. ('phone', 'Phone Number'),
  19. ('group', 'WhatsApp Group')
  20. ], string='Recipient Type', default='phone', help="Type of recipient: phone number or WhatsApp group")
  21. whatsapp_group_id = fields.Many2one('ww.group', string='WhatsApp Group',
  22. help="WhatsApp group to send message to (if recipient_type is group)",
  23. ondelete='set null')
  24. @api.depends('recipient_type', 'mobile_number', 'whatsapp_group_id')
  25. def _compute_final_recipient(self):
  26. """Compute the final recipient based on type"""
  27. for record in self:
  28. if record.recipient_type == 'group' and record.whatsapp_group_id:
  29. record.final_recipient = record.whatsapp_group_id.whatsapp_web_id
  30. else:
  31. record.final_recipient = record.mobile_number
  32. final_recipient = fields.Char('Final Recipient', compute='_compute_final_recipient',
  33. help="Final recipient (phone or group ID)")
  34. @api.depends('mobile_number', 'recipient_type')
  35. def _compute_mobile_number_formatted(self):
  36. """Override SOLO para casos específicos de grupos con WhatsApp Web"""
  37. for message in self:
  38. # SOLO intervenir si es grupo CON WhatsApp Web configurado
  39. if (hasattr(message, 'recipient_type') and message.recipient_type == 'group' and
  40. message.wa_account_id and message.wa_account_id.whatsapp_web_url and
  41. message.mobile_number and message.mobile_number.endswith('@g.us')):
  42. message.mobile_number_formatted = message.mobile_number
  43. else:
  44. # TODOS LOS DEMÁS CASOS: usar lógica original sin modificar
  45. super(WhatsAppMessage, message)._compute_mobile_number_formatted()
  46. @api.constrains('recipient_type', 'mobile_number', 'whatsapp_group_id')
  47. def _check_recipient_configuration(self):
  48. """Validar configuración de destinatario"""
  49. for record in self:
  50. if record.recipient_type == 'group':
  51. if not record.whatsapp_group_id and not (record.mobile_number and record.mobile_number.endswith('@g.us')):
  52. raise ValidationError("Para mensajes a grupos, debe seleccionar un grupo o proporcionar un ID de grupo válido (@g.us)")
  53. elif record.recipient_type == 'phone':
  54. if not record.mobile_number or record.mobile_number.endswith('@g.us'):
  55. raise ValidationError("Para mensajes a teléfonos, debe proporcionar un número telefónico válido")
  56. @api.onchange('recipient_type')
  57. def _onchange_recipient_type(self):
  58. """Limpiar campos al cambiar tipo de destinatario"""
  59. if self.recipient_type == 'group':
  60. self.mobile_number = False
  61. else:
  62. self.whatsapp_group_id = False
  63. def _whatsapp_phone_format(self, fpath=None, number=None, raise_on_format_error=False):
  64. """Override SOLO para casos específicos de grupos - NO interferir con funcionalidad nativa"""
  65. self.ensure_one()
  66. # SOLO intervenir en casos muy específicos de grupos
  67. # Si es un mensaje a grupo Y tiene WhatsApp Web configurado
  68. if (hasattr(self, 'recipient_type') and self.recipient_type == 'group' and
  69. self.wa_account_id and self.wa_account_id.whatsapp_web_url):
  70. if self.whatsapp_group_id:
  71. return self.whatsapp_group_id.whatsapp_web_id
  72. # Si el número es un ID de grupo (termina en @g.us), retornarlo sin validar
  73. elif number and number.endswith('@g.us'):
  74. return number
  75. elif self.mobile_number and self.mobile_number.endswith('@g.us'):
  76. return self.mobile_number
  77. # TODOS LOS DEMÁS CASOS: usar validación original sin modificar
  78. return super()._whatsapp_phone_format(fpath, number, raise_on_format_error)
  79. def _get_final_destination(self):
  80. """Método mejorado para obtener destino final (grupo o teléfono)"""
  81. self.ensure_one()
  82. # 1. Si es tipo grupo y hay grupo seleccionado
  83. if self.recipient_type == 'group' and self.whatsapp_group_id:
  84. try:
  85. return self.whatsapp_group_id.whatsapp_web_id
  86. except Exception:
  87. # Si el modelo ww.group no existe, usar mobile_number
  88. pass
  89. # 2. Si el mobile_number es un ID de grupo
  90. if self.mobile_number and self.mobile_number.endswith('@g.us'):
  91. return self.mobile_number
  92. return False
  93. def _send_message(self, with_commit=False):
  94. url = ''
  95. session_name = ''
  96. api_key = ''
  97. if self.wa_account_id and self.wa_account_id.whatsapp_web_url:
  98. url = self.wa_account_id.whatsapp_web_url
  99. session_name = self.wa_account_id.whatsapp_web_login or ''
  100. api_key = self.wa_account_id.whatsapp_web_api_key or ''
  101. _logger.info('WHATSAPP WEB SEND MESSAGE - URL: %s, Session: %s', url, session_name)
  102. group = ''
  103. if not url or not session_name or not api_key:
  104. # Si no hay configuración de WhatsApp Web, usar método original
  105. super()._send_message(with_commit)
  106. return
  107. for whatsapp_message in self:
  108. # Determinar destinatario final usando solo la nueva lógica
  109. final_destination = whatsapp_message._get_final_destination()
  110. if final_destination:
  111. group = final_destination
  112. attachment = False
  113. if whatsapp_message.wa_template_id:
  114. record = self.env[whatsapp_message.wa_template_id.model].browse(whatsapp_message.mail_message_id.res_id)
  115. #codigo con base a whatsapp.message y whatsapp.template para generacion de adjuntos
  116. RecordModel = self.env[whatsapp_message.mail_message_id.model].with_user(whatsapp_message.create_uid)
  117. from_record = RecordModel.browse(whatsapp_message.mail_message_id.res_id)
  118. # if retrying message then we need to unlink previous attachment
  119. # in case of header with report in order to generate it again
  120. 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:
  121. whatsapp_message.mail_message_id.attachment_ids.unlink()
  122. if not attachment and whatsapp_message.wa_template_id.report_id:
  123. attachment = whatsapp_message.wa_template_id._generate_attachment_from_report(record)
  124. if not attachment and whatsapp_message.wa_template_id.header_attachment_ids:
  125. attachment = whatsapp_message.wa_template_id.header_attachment_ids[0]
  126. if attachment and attachment not in whatsapp_message.mail_message_id.attachment_ids:
  127. whatsapp_message.mail_message_id.attachment_ids = [(4, attachment.id)]
  128. # no template
  129. elif whatsapp_message.mail_message_id.attachment_ids:
  130. attachment = whatsapp_message.mail_message_id.attachment_ids[0]
  131. #codigo para limpiar body y numero
  132. body = whatsapp_message.body
  133. # Asegurar que body sea string y limpiar HTML
  134. if body:
  135. if isinstance(body, markupsafe.Markup):
  136. text = html.unescape(str(body))
  137. else:
  138. text = str(body)
  139. # Reemplazamos las etiquetas BR y P
  140. text = re.sub(r'<br\s*/?>|<BR\s*/?>', '\n', text)
  141. text = re.sub(r'<p>|<P>', '\n\n', text)
  142. text = re.sub(r'</p>|</P>', '', text)
  143. # Eliminamos el resto de etiquetas HTML
  144. text = re.sub(r'<[^>]+>', '', text)
  145. # Limpiamos múltiples saltos de línea
  146. text = re.sub(r'\n\s*\n\s*\n', '\n\n', text)
  147. # Limpiamos espacios en blanco al inicio y final
  148. body = text.strip()
  149. # Asegurar que no esté vacío
  150. if not body:
  151. body = "Mensaje de WhatsApp"
  152. else:
  153. body = "Mensaje de WhatsApp"
  154. # Determinar número/destinatario final
  155. if group:
  156. # Si ya hay un grupo determinado, usarlo
  157. number = group
  158. else:
  159. # Formatear número según el tipo de destinatario
  160. if whatsapp_message.recipient_type == 'group':
  161. if whatsapp_message.whatsapp_group_id:
  162. number = whatsapp_message.whatsapp_group_id.whatsapp_web_id
  163. elif whatsapp_message.mobile_number and whatsapp_message.mobile_number.endswith('@g.us'):
  164. number = whatsapp_message.mobile_number
  165. else:
  166. _logger.error("Mensaje configurado como grupo pero sin destinatario válido")
  167. continue
  168. else:
  169. # Lógica original para números de teléfono
  170. number = whatsapp_message.mobile_number
  171. if number:
  172. number = number.replace(' ', '').replace('+','').replace('-','')
  173. if number.startswith("52") and len(number) == 12:
  174. number = "521" + number[2:]
  175. if len(number) == 10:
  176. number = "521" + number
  177. number = number + '@c.us'
  178. # ENVIO DE MENSAJE - Nueva API Gateway
  179. parent_message_id = ''
  180. if whatsapp_message.mail_message_id and whatsapp_message.mail_message_id.parent_id:
  181. parent_id = whatsapp_message.mail_message_id.parent_id.wa_message_ids
  182. if parent_id:
  183. parent_message_id = parent_id[0].msg_uid
  184. # Validar que tenemos un destinatario válido
  185. if not number:
  186. _logger.error("No se pudo determinar el destinatario para el mensaje")
  187. continue
  188. # Validar que tenemos un cuerpo de mensaje válido
  189. if not body or not isinstance(body, str):
  190. _logger.error("Cuerpo del mensaje inválido: %s", body)
  191. body = "Mensaje de WhatsApp"
  192. # Determinar si es grupo
  193. is_group = number.endswith('@g.us') if number else False
  194. # Construir URL base
  195. base_url = url.rstrip('/')
  196. endpoint = 'send-message'
  197. # Headers con autenticación
  198. headers = {
  199. "Content-Type": "application/json",
  200. "X-API-Key": api_key
  201. }
  202. # Preparar payload según tipo de mensaje
  203. if attachment:
  204. # Determinar endpoint según tipo de archivo
  205. mimetype = attachment.mimetype or 'application/octet-stream'
  206. if mimetype.startswith('image/'):
  207. endpoint = 'send-image'
  208. elif mimetype.startswith('video/'):
  209. endpoint = 'send-video'
  210. elif mimetype.startswith('audio/'):
  211. endpoint = 'send-voice'
  212. else:
  213. endpoint = 'send-file'
  214. # Convertir archivo a base64 con prefijo data URI
  215. file_base64 = base64.b64encode(attachment.raw).decode('utf-8')
  216. base64_with_prefix = f"data:{mimetype};base64,{file_base64}"
  217. payload = {
  218. "phone": [number], # Array para send-image/send-file
  219. "base64": base64_with_prefix,
  220. "filename": attachment.name or "file",
  221. "caption": body,
  222. "isGroup": is_group
  223. }
  224. if parent_message_id:
  225. payload["quotedMessageId"] = parent_message_id
  226. else:
  227. # Mensaje de texto
  228. payload = {
  229. "phone": number, # String para send-message
  230. "message": body,
  231. "isGroup": is_group
  232. }
  233. if parent_message_id:
  234. payload["quotedMessageId"] = parent_message_id
  235. # Construir URL completa
  236. full_url = f"{base_url}/api/v1/{session_name}/{endpoint}"
  237. # Log del payload para debugging
  238. _logger.info("Enviando mensaje a %s (%s) usando endpoint %s", number, "grupo" if is_group else "contacto", endpoint)
  239. # Realizar petición POST
  240. try:
  241. response = requests.post(full_url, json=payload, headers=headers, timeout=60)
  242. # Procesar respuesta
  243. if response.status_code == 200:
  244. try:
  245. response_json = response.json()
  246. # La nueva API puede devolver jobId (mensaje encolado) o id (enviado directamente)
  247. if 'jobId' in response_json:
  248. # Mensaje encolado
  249. _logger.info("Mensaje encolado. Job ID: %s", response_json.get('jobId'))
  250. whatsapp_message.write({
  251. 'state': 'outgoing', # Mantener 'outgoing' para compatibilidad
  252. 'msg_uid': response_json.get('jobId')
  253. })
  254. self._cr.commit()
  255. elif 'id' in response_json:
  256. # Mensaje enviado directamente
  257. msg_id = response_json.get('id')
  258. if isinstance(msg_id, dict) and '_serialized' in msg_id:
  259. msg_uid = msg_id['_serialized']
  260. elif isinstance(msg_id, str):
  261. msg_uid = msg_id
  262. else:
  263. msg_uid = str(msg_id)
  264. _logger.info("Mensaje enviado exitosamente. ID: %s", msg_uid)
  265. whatsapp_message.write({
  266. 'state': 'sent',
  267. 'msg_uid': msg_uid
  268. })
  269. self._cr.commit()
  270. else:
  271. _logger.warning("Respuesta exitosa pero sin jobId ni id: %s", response_json)
  272. whatsapp_message.write({
  273. 'state': 'outgoing'
  274. })
  275. self._cr.commit()
  276. except ValueError:
  277. _logger.error("La respuesta no es JSON válido: %s", response.text)
  278. whatsapp_message.write({
  279. 'state': 'error'
  280. })
  281. self._cr.commit()
  282. else:
  283. _logger.error("Error en la petición. Código: %s, Respuesta: %s", response.status_code, response.text)
  284. whatsapp_message.write({
  285. 'state': 'error'
  286. })
  287. self._cr.commit()
  288. except requests.exceptions.RequestException as e:
  289. _logger.error("Error de conexión al enviar mensaje: %s", str(e))
  290. whatsapp_message.write({
  291. 'state': 'error'
  292. })
  293. self._cr.commit()
  294. time.sleep(random.randint(3, 7))