ww_group.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. from odoo import models, fields, api
  2. import logging
  3. from datetime import datetime
  4. _logger = logging.getLogger(__name__)
  5. class WWGroup(models.Model):
  6. _name = "ww.group"
  7. _description = "Grupo de WhatsApp Web"
  8. name = fields.Char(string="Nombre del Grupo", required=True)
  9. whatsapp_web_id = fields.Char(
  10. string="ID WhatsApp Web", index=True, help="ID único del grupo en WhatsApp Web"
  11. )
  12. whatsapp_account_id = fields.Many2one(
  13. "whatsapp.account", string="Cuenta de WhatsApp", required=True
  14. )
  15. channel_id = fields.Many2one(
  16. "discuss.channel", string="Canal de Discusión", readonly=True
  17. )
  18. contact_ids = fields.Many2many(
  19. comodel_name="res.partner",
  20. relation="ww_group_contact_rel",
  21. column1="group_id",
  22. column2="contact_id",
  23. string="Contactos",
  24. readonly=True,
  25. )
  26. def _process_messages(self, messages_data):
  27. """Process WhatsApp messages and create them in the channel"""
  28. self.ensure_one()
  29. if not messages_data or not self.channel_id:
  30. return True
  31. # Get existing message IDs to avoid duplicates
  32. existing_ids = set(self.channel_id.message_ids.mapped("message_id"))
  33. # Prepare bulk create values
  34. message_vals_list = []
  35. for msg_data in messages_data:
  36. msg_id = msg_data.get("id", {}).get("_serialized")
  37. # Skip if message already exists
  38. if msg_id in existing_ids:
  39. continue
  40. # Get author partner
  41. author_whatsapp_id = msg_data.get("author")
  42. author = (
  43. self.env["res.partner"].search(
  44. [("whatsapp_web_id", "=", author_whatsapp_id)], limit=1
  45. )
  46. if author_whatsapp_id
  47. else False
  48. )
  49. # Get quoted message author if exists
  50. quoted_author = False
  51. if msg_data.get("hasQuotedMsg") and msg_data.get("quotedParticipant"):
  52. quoted_author = self.env["res.partner"].search(
  53. [("whatsapp_web_id", "=", msg_data["quotedParticipant"])], limit=1
  54. )
  55. # Convert timestamp to datetime
  56. timestamp = datetime.fromtimestamp(msg_data.get("timestamp", 0))
  57. # Prepare message body with author and content
  58. author_name = author.name if author else "Desconocido"
  59. message_body = f"{msg_data.get('body', '')}"
  60. # Add quoted message if exists
  61. if msg_data.get("hasQuotedMsg") and msg_data.get("quotedMsg", {}).get(
  62. "body"
  63. ):
  64. quoted_author_name = (
  65. quoted_author.name if quoted_author else "Desconocido"
  66. )
  67. message_body += f"\n\n<blockquote><strong>{quoted_author_name}:</strong> {msg_data['quotedMsg']['body']}</blockquote>"
  68. message_vals = {
  69. "model": "discuss.channel",
  70. "res_id": self.channel_id.id,
  71. "message_type": "comment",
  72. "subtype_id": self.env.ref("mail.mt_comment").id,
  73. "body": message_body,
  74. "date": timestamp,
  75. "author_id": author.id if author else self.env.user.partner_id.id,
  76. "message_id": msg_id,
  77. }
  78. message_vals_list.append(message_vals)
  79. # Bulk create messages
  80. if message_vals_list:
  81. self.env["mail.message"].create(message_vals_list)
  82. return True
  83. def _create_discussion_channel(self):
  84. """Create a discussion channel for the WhatsApp group"""
  85. self.ensure_one()
  86. try:
  87. # Verificar si ya existe un canal para este grupo
  88. if self.channel_id:
  89. return self.channel_id
  90. # Create channel name with WhatsApp prefix
  91. channel_name = f"📱 {self.name}"
  92. # Obtener los IDs de los contactos de forma segura
  93. partner_ids = []
  94. if self.contact_ids:
  95. for contact in self.contact_ids:
  96. if contact and contact.id:
  97. partner_ids.append(contact.id)
  98. # Create the channel using standard create method for maximum robustness
  99. _logger.info(f"WWGroup: Creating channel via create() for {self.name}")
  100. # Default values for a public channel in WhatsApp section
  101. # We use channel_type='whatsapp' so it appears in the correct UI section
  102. channel_vals = {
  103. "name": channel_name,
  104. "channel_type": "whatsapp",
  105. "whatsapp_number": self.whatsapp_web_id, # @g.us ID
  106. "wa_account_id": self.whatsapp_account_id.id,
  107. "group_public_id": None,
  108. }
  109. channel = self.env["discuss.channel"].create(channel_vals)
  110. _logger.info(f"WWGroup: WhatsApp Channel created with ID {channel.id}")
  111. # Add members to the channel if any
  112. # AUTOMATICALLY ADD system user / account owner so they can see the channel
  113. if self.whatsapp_account_id and self.whatsapp_account_id.create_uid:
  114. owner_partner = self.whatsapp_account_id.create_uid.partner_id
  115. if owner_partner and owner_partner.id not in partner_ids:
  116. partner_ids.append(owner_partner.id)
  117. if partner_ids:
  118. _logger.info(
  119. f"WWGroup: Adding {len(partner_ids)} members to channel {channel.id}"
  120. )
  121. channel.add_members(partner_ids=partner_ids)
  122. # Link the channel to the group
  123. self.write({"channel_id": channel.id})
  124. return channel
  125. except Exception as e:
  126. _logger.error(
  127. f"Error al crear el canal para el grupo {self.name}: {str(e)}",
  128. exc_info=True,
  129. )
  130. return False
  131. except Exception as e:
  132. _logger.error(
  133. f"Error al crear el canal para el grupo {self.name}: {str(e)}"
  134. )
  135. return False
  136. def process_webhook_group(self, account, group_metadata, sender_partner=False):
  137. """
  138. Process group metadata from webhook to Create/Update group and handle members gracefully.
  139. Args:
  140. account: whatsapp.account record
  141. group_metadata: dict {'id':..., 'name':...}
  142. sender_partner: res.partner record (the sender of the message)
  143. Returns:
  144. ww.group record or False
  145. """
  146. try:
  147. group_id = group_metadata.get("id")
  148. group_name = group_metadata.get("name", "Sin nombre")
  149. if not group_id:
  150. return False
  151. # Buscar o crear grupo
  152. group = self.search(
  153. [
  154. ("whatsapp_web_id", "=", group_id),
  155. ("whatsapp_account_id", "=", account.id),
  156. ],
  157. limit=1,
  158. )
  159. if not group:
  160. group = self.create(
  161. {
  162. "name": group_name,
  163. "whatsapp_web_id": group_id,
  164. "whatsapp_account_id": account.id,
  165. }
  166. )
  167. else:
  168. # Actualizar nombre del grupo si cambió y no es el default
  169. if group.name != group_name and group_name != "Sin nombre":
  170. group.write({"name": group_name})
  171. # Actualizar nombre del canal si existe
  172. if group.channel_id:
  173. group.channel_id.write({"name": f"📱 {group_name}"})
  174. # Organic Member Growth: Add sender to group if not present
  175. if sender_partner:
  176. if sender_partner.id not in group.contact_ids.ids:
  177. _logger.info(
  178. f"Adding sender {sender_partner.name} to group {group.name}"
  179. )
  180. group.write({"contact_ids": [(4, sender_partner.id)]})
  181. # Ensure channel exists (Lazy creation)
  182. if not group.channel_id:
  183. group._create_discussion_channel()
  184. # Organic Member Growth: Add sender to channel if not present
  185. if group.channel_id and sender_partner:
  186. # Check membership (this check might be redundant as add_members handles duplication, but safe)
  187. group.channel_id.add_members(partner_ids=[sender_partner.id])
  188. return group
  189. except Exception as e:
  190. _logger.error(f"Error processing webhook group {group_metadata}: {e}")
  191. return False
  192. def process_group_data(self, account, group_data):
  193. """
  194. Process a single group data (from API SYNC): create/update group, sync participants.
  195. Returns the group object or False.
  196. """
  197. try:
  198. group_id = group_data.get("id").get("_serialized")
  199. group_name = group_data.get("name", "Sin nombre")
  200. # Buscar o crear grupo
  201. group = self.search(
  202. [
  203. ("whatsapp_web_id", "=", group_id),
  204. ("whatsapp_account_id", "=", account.id),
  205. ],
  206. limit=1,
  207. )
  208. if not group:
  209. group = self.create(
  210. {
  211. "name": group_name,
  212. "whatsapp_web_id": group_id,
  213. "whatsapp_account_id": account.id,
  214. }
  215. )
  216. else:
  217. # Actualizar nombre del grupo si cambió
  218. if group.name != group_name:
  219. group.write({"name": group_name})
  220. # Actualizar nombre del canal si existe
  221. if group.channel_id:
  222. group.channel_id.write({"name": f"📱 {group_name}"})
  223. # Procesar participantes del grupo
  224. participants = group_data.get("members", [])
  225. contact_ids = []
  226. # Log para debug
  227. _logger.info(
  228. f"Procesando grupo {group_name}: {len(participants)} participantes encontrados"
  229. )
  230. for participant in participants:
  231. whatsapp_web_id = participant.get("id", {}).get("_serialized")
  232. mobile = participant.get("number", "")
  233. # Derive participant name
  234. participant_name = (
  235. participant.get("name") or participant.get("pushname") or mobile
  236. )
  237. # Search for existing contact
  238. contact = self.env["res.partner"].search(
  239. [("whatsapp_web_id", "=", whatsapp_web_id)], limit=1
  240. )
  241. if not contact and mobile and len(mobile) >= 10:
  242. last_10_digits = mobile[-10:]
  243. contact = self.env["res.partner"].search(
  244. [("mobile", "like", "%" + last_10_digits)], limit=1
  245. )
  246. partner_vals = {
  247. "name": participant_name,
  248. "mobile": mobile,
  249. "whatsapp_web_id": whatsapp_web_id,
  250. }
  251. if contact:
  252. # Update existing contact
  253. contact.write(partner_vals)
  254. else:
  255. # Create new contact
  256. contact = self.env["res.partner"].create(partner_vals)
  257. if contact:
  258. contact_ids.append(contact.id)
  259. # Actualizar contactos del grupo
  260. group.write({"contact_ids": [(6, 0, contact_ids)]})
  261. # Update discussion channel members
  262. if not group.channel_id:
  263. group._create_discussion_channel()
  264. else:
  265. group._update_discussion_channel()
  266. # Process messages if available
  267. messages = group_data.get("messages", [])
  268. if messages:
  269. group._process_messages(messages)
  270. return group
  271. except Exception as e:
  272. _logger.error(
  273. f"Error procesando datos del grupo {group_data.get('name')}: {str(e)}"
  274. )
  275. return False
  276. @api.model
  277. def sync_ww_contacts_groups(self):
  278. """
  279. Sincroniza los contactos y grupos de WhatsApp Web.
  280. Solo sincroniza contactos que están dentro de grupos y valida que no se dupliquen,
  281. verificando los últimos 10 dígitos del campo mobile.
  282. """
  283. accounts = self.env["whatsapp.account"].search([])
  284. for account in accounts:
  285. try:
  286. # Obtener grupos usando el método de la cuenta
  287. groups_data = account.get_groups()
  288. if not groups_data:
  289. continue
  290. # Procesar cada grupo
  291. for group_data in groups_data:
  292. self.process_group_data(account, group_data)
  293. except Exception as e:
  294. _logger.error(
  295. "Error en la sincronización de grupos para la cuenta %s: %s",
  296. account.name,
  297. str(e),
  298. )
  299. continue
  300. return True
  301. def send_whatsapp_message(self, body, attachment=None, wa_template_id=None):
  302. """Enviar mensaje WhatsApp al grupo"""
  303. self.ensure_one()
  304. if not self.whatsapp_account_id:
  305. raise ValueError("Group must have a WhatsApp account configured")
  306. # Crear mail.message si hay adjunto
  307. mail_message = None
  308. if attachment:
  309. mail_message = self.env["mail.message"].create(
  310. {"body": body, "attachment_ids": [(4, attachment.id)]}
  311. )
  312. # Crear mensaje WhatsApp
  313. message_vals = {
  314. "body": body,
  315. "recipient_type": "group",
  316. "whatsapp_group_id": self.id,
  317. "mobile_number": self.whatsapp_web_id,
  318. "wa_account_id": self.whatsapp_account_id.id,
  319. "state": "outgoing",
  320. }
  321. if mail_message:
  322. message_vals["mail_message_id"] = mail_message.id
  323. if wa_template_id:
  324. message_vals["wa_template_id"] = wa_template_id
  325. whatsapp_message = self.env["whatsapp.message"].create(message_vals)
  326. # Enviar mensaje
  327. whatsapp_message._send_message()
  328. return whatsapp_message
  329. def action_send_whatsapp_message(self):
  330. """Acción para abrir el composer de WhatsApp para el grupo"""
  331. self.ensure_one()
  332. return {
  333. "type": "ir.actions.act_window",
  334. "name": f"Send Message to {self.name}",
  335. "res_model": "whatsapp.composer",
  336. "view_mode": "form",
  337. "target": "new",
  338. "context": {
  339. "default_recipient_type": "group",
  340. "default_whatsapp_group_id": self.id,
  341. "default_wa_template_id": False,
  342. },
  343. }