|
@@ -247,11 +247,11 @@ class WhatsAppMessage(models.Model):
|
|
|
# Limpiamos espacios en blanco al inicio y final
|
|
# Limpiamos espacios en blanco al inicio y final
|
|
|
body = text.strip()
|
|
body = text.strip()
|
|
|
|
|
|
|
|
- # Asegurar que no esté vacío
|
|
|
|
|
|
|
+ # Asegurar que no esté vacío (solo si no hay adjunto)
|
|
|
if not body:
|
|
if not body:
|
|
|
- body = "Mensaje de WhatsApp"
|
|
|
|
|
|
|
+ body = "" if attachment else "Mensaje de WhatsApp"
|
|
|
else:
|
|
else:
|
|
|
- body = "Mensaje de WhatsApp"
|
|
|
|
|
|
|
+ body = "" if attachment else "Mensaje de WhatsApp"
|
|
|
|
|
|
|
|
# Determinar número/destinatario final
|
|
# Determinar número/destinatario final
|
|
|
if group:
|
|
if group:
|
|
@@ -302,9 +302,12 @@ class WhatsAppMessage(models.Model):
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
# Validar que tenemos un cuerpo de mensaje válido
|
|
# 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)
|
|
_logger.error("Cuerpo del mensaje inválido: %s", body)
|
|
|
body = "Mensaje de WhatsApp"
|
|
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
|
|
# Determinar si es grupo
|
|
|
is_group = number.endswith("@g.us") if number else False
|
|
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.
|
|
y actualizamos el msg_uid al ID real de WhatsApp antes de procesar el estado.
|
|
|
"""
|
|
"""
|
|
|
# Pre-process statuses to reconcile IDs
|
|
# Pre-process statuses to reconcile IDs
|
|
|
|
|
+ metadata = value.get("metadata", {})
|
|
|
|
|
+ job_id = metadata.get("job_id")
|
|
|
|
|
+
|
|
|
for status in value.get("statuses", []):
|
|
for status in value.get("statuses", []):
|
|
|
real_wa_id = status.get("id")
|
|
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:
|
|
if job_id and real_wa_id:
|
|
|
# Buscar mensaje por job_id que tenga un msg_uid incorrecto (el del worker)
|
|
# 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):
|
|
def _update_message_fetched_seen(self):
|
|
|
"""
|
|
"""
|
|
|
Sobrescritura para manejar concurrencia en la actualización de discuss.channel.member.
|
|
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()
|
|
self.ensure_one()
|
|
|
if self.mail_message_id.model != "discuss.channel":
|
|
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)
|
|
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(
|
|
channel_member = channel.channel_member_ids.filtered(
|
|
|
lambda cm: cm.partner_id == channel.whatsapp_partner_id
|
|
lambda cm: cm.partner_id == channel.whatsapp_partner_id
|
|
|
)
|
|
)
|
|
@@ -516,39 +517,52 @@ class WhatsAppMessage(models.Model):
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
channel_member = channel_member[0]
|
|
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
|
|
notification_type = None
|
|
|
|
|
+ updated_rows = 0
|
|
|
|
|
+
|
|
|
if self.state == "read":
|
|
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":
|
|
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:
|
|
if notification_type:
|
|
|
channel._bus_send(
|
|
channel._bus_send(
|