2 Commits 0537d89930 ... 2100f4a10a

Autore SHA1 Messaggio Data
  odoo 2100f4a10a chore: commit pending changes before subtree update 1 mese fa
  odoo c4c2695610 fix(whatsapp): resolve serialization failure and owl error in discuss 1 mese fa

+ 1 - 0
__manifest__.py

@@ -19,6 +19,7 @@
         "web.assets_backend": [
             "whatsapp_web/static/src/overrides/thread_model_patch.js",
             "whatsapp_web/static/src/overrides/composer_patch.js",
+            "whatsapp_web/static/src/overrides/channel_member_list_patch.js",
         ],
     },
     "installable": True,

+ 2 - 2
models/whatsapp_account.py

@@ -264,7 +264,7 @@ class WhatsAppAccount(models.Model):
             if not group_metadata and value.get("metadata", {}).get("group_id"):
                 group_metadata = {"id": value.get("metadata", {}).get("group_id")}
 
-            if not channel and group_metadata:
+            if group_metadata:
                 # Check if group module is installed (Use 'in' operator for models with dots)
                 if "ww.group" in self.env:
                     # Process Group (Lazy Create + Organic Member Add)
@@ -272,7 +272,7 @@ class WhatsAppAccount(models.Model):
                         self, group_metadata, sender_partner
                     )
 
-                    if group and group.channel_id:
+                    if group and group.channel_id and not channel:
                         channel = group.channel_id
                         _logger.info(
                             "Mensaje de grupo ruteado a canal: %s", channel.name

+ 53 - 39
models/whatsapp_message.py

@@ -247,11 +247,11 @@ class WhatsAppMessage(models.Model):
                 # Limpiamos espacios en blanco al inicio y final
                 body = text.strip()
 
-                # Asegurar que no esté vacío
+                # Asegurar que no esté vacío (solo si no hay adjunto)
                 if not body:
-                    body = "Mensaje de WhatsApp"
+                    body = "" if attachment else "Mensaje de WhatsApp"
             else:
-                body = "Mensaje de WhatsApp"
+                body = "" if attachment else "Mensaje de WhatsApp"
 
             # Determinar número/destinatario final
             if group:
@@ -302,9 +302,12 @@ class WhatsAppMessage(models.Model):
                 continue
 
             # Validar que tenemos un cuerpo de mensaje válido
-            if not body or not isinstance(body, str):
+            if not body and not attachment:
                 _logger.error("Cuerpo del mensaje inválido: %s", body)
                 body = "Mensaje de WhatsApp"
+            elif body and not isinstance(body, str):
+                _logger.error("Cuerpo del mensaje inválido (tipo incorrecto): %s", body)
+                body = "Mensaje de WhatsApp"
 
             # Determinar si es grupo
             is_group = number.endswith("@g.us") if number else False
@@ -459,12 +462,11 @@ class WhatsAppMessage(models.Model):
         y actualizamos el msg_uid al ID real de WhatsApp antes de procesar el estado.
         """
         # Pre-process statuses to reconcile IDs
+        metadata = value.get("metadata", {})
+        job_id = metadata.get("job_id")
+
         for status in value.get("statuses", []):
             real_wa_id = status.get("id")
-            # Buscar job_id en metadata (según requerimiento del usuario)
-            # Estructura esperada: metadata: { job_id: "..." }
-            metadata = status.get("metadata", {})
-            job_id = metadata.get("job_id")
 
             if job_id and real_wa_id:
                 # Buscar mensaje por job_id que tenga un msg_uid incorrecto (el del worker)
@@ -498,7 +500,7 @@ class WhatsAppMessage(models.Model):
     def _update_message_fetched_seen(self):
         """
         Sobrescritura para manejar concurrencia en la actualización de discuss.channel.member.
-        Usa bloqueo de fila (SELECT FOR UPDATE) para evitar SERIALIZATION_FAILURE.
+        Usa bloqueo de fila (NO KEY UPDATE SKIP LOCKED) para evitar SERIALIZATION_FAILURE.
         """
         self.ensure_one()
         if self.mail_message_id.model != "discuss.channel":
@@ -506,8 +508,7 @@ class WhatsAppMessage(models.Model):
 
         channel = self.env["discuss.channel"].browse(self.mail_message_id.res_id)
 
-        # Buscar el miembro usando SQL directo para bloquear la fila antes de leer/escribir
-        # Odoo ORM no soporta lock explícito fácil en search(), así que primero buscamos ID y luego bloqueamos.
+        # Buscar el miembro asociado al partner de WhatsApp
         channel_member = channel.channel_member_ids.filtered(
             lambda cm: cm.partner_id == channel.whatsapp_partner_id
         )
@@ -516,39 +517,52 @@ class WhatsAppMessage(models.Model):
             return
 
         channel_member = channel_member[0]
-
-        # --- CONCURRENCY FIX START ---
-        try:
-            # Bloquear la fila específica del miembro del canal
-            self.env.cr.execute(
-                "SELECT id FROM discuss_channel_member WHERE id = %s FOR UPDATE",
-                (channel_member.id,),
-            )
-            # Invalidar caché para asegurar que leemos datos frescos después del bloqueo
-            channel_member.invalidate_recordset()
-        except Exception as e:
-            _logger.warning(
-                "No se pudo bloquear discuss_channel_member %s: %s",
-                channel_member.id,
-                e,
-            )
-        # --- CONCURRENCY FIX END ---
+        member_id = channel_member.id
 
         notification_type = None
+        updated_rows = 0
+
         if self.state == "read":
-            channel_member.write(
-                {
-                    "fetched_message_id": max(
-                        channel_member.fetched_message_id.id, self.mail_message_id.id
-                    ),
-                    "seen_message_id": self.mail_message_id.id,
-                    "last_seen_dt": fields.Datetime.now(),
-                }
+            # Intentar actualizar usando SKIP LOCKED
+            # Si el registro está bloqueado, simplemente saltamos esta actualización
+            # ya que fetched/seen son contadores monotónicos o de estado reciente
+            self.env.cr.execute(
+                """
+                UPDATE discuss_channel_member
+                SET fetched_message_id = GREATEST(fetched_message_id, %s),
+                    seen_message_id = %s,
+                    last_seen_dt = NOW()
+                WHERE id IN (
+                    SELECT id FROM discuss_channel_member WHERE id = %s
+                    FOR NO KEY UPDATE SKIP LOCKED
+                )
+                RETURNING id
+                """,
+                (self.mail_message_id.id, self.mail_message_id.id, member_id),
             )
-            notification_type = "discuss.channel.member/seen"
+            # Si fetchone devuelve algo, significa que actualizó. Si es None, saltó por bloqueo.
+            if self.env.cr.fetchone():
+                channel_member.invalidate_recordset(
+                    ["fetched_message_id", "seen_message_id", "last_seen_dt"]
+                )
+                notification_type = "discuss.channel.member/seen"
+
         elif self.state == "delivered":
-            channel_member.write({"fetched_message_id": self.mail_message_id.id})
-            notification_type = "discuss.channel.member/fetched"
+            self.env.cr.execute(
+                """
+                UPDATE discuss_channel_member
+                SET fetched_message_id = GREATEST(fetched_message_id, %s)
+                WHERE id IN (
+                    SELECT id FROM discuss_channel_member WHERE id = %s
+                    FOR NO KEY UPDATE SKIP LOCKED
+                )
+                RETURNING id
+                """,
+                (self.mail_message_id.id, member_id),
+            )
+            if self.env.cr.fetchone():
+                channel_member.invalidate_recordset(["fetched_message_id"])
+                notification_type = "discuss.channel.member/fetched"
 
         if notification_type:
             channel._bus_send(

+ 20 - 0
static/src/overrides/channel_member_list_patch.js

@@ -0,0 +1,20 @@
+/* @odoo-module */
+
+import { ChannelMemberList } from "@mail/discuss/core/common/channel_member_list";
+import { patch } from "@web/core/utils/patch";
+
+patch(ChannelMemberList.prototype, {
+    /**
+     * Override to prevent crash when clicking on a member without a system user ID.
+     * WhatsApp contacts are partners but not users, so they don't have a userId.
+     * The AvatarCardPopover requires a valid userId.
+     */
+    onClickAvatar(ev, member) {
+        if (!member.persona.userId) {
+            // If the member has no userId (e.g. WhatsApp contact), do not open the popover.
+            // This prevents the "Invalid props: id is not a number" error.
+            return;
+        }
+        super.onClickAvatar(ev, member);
+    },
+});