Oleada UX 7: Chat (presencia, busqueda en historial) y WACRM (metricas para gerencia, notas internas y etiquetas que sobreviven al sync) + fixes de concurrencia

This commit is contained in:
2026-09-17 23:46:40 +00:00
parent 31c92ac419
commit c38783eb31
13 changed files with 1589 additions and 36 deletions

View File

@@ -2,3 +2,4 @@ from . import main
from . import wacrm_sync
from . import frontend_user
from . import config_audit
from . import wacrm_notes

View File

@@ -27,6 +27,10 @@ class SkeenFrontendUser(models.Model):
active = fields.Boolean(string='Activo', default=True)
must_change_password = fields.Boolean(string='Debe cambiar contraseña', default=False)
last_login = fields.Datetime(string='Último acceso', readonly=True)
# Presencia: se actualiza con cada llamada autenticada del frontend
# (heartbeat explícito cada ~30 s y en cada endpoint de chat).
# "En línea" = actividad en los últimos 2 minutos.
last_seen = fields.Datetime(string='Última actividad', readonly=True, index=True)
notes = fields.Text(string='Notas')
allowed_menus = fields.Text(string='Menús Permitidos',
help='JSON array de hrefs visibles; vacío = defaults por rol')

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class SkeenWacrmNote(models.Model):
"""Nota interna de una conversación de WhatsApp.
Se liga por `external_id` de la conversación (NO por many2one al caché
local) a propósito: el sync borra y recrea el caché de conversaciones y
mensajes, y las notas deben sobrevivir. Nunca se envía al paciente.
"""
_name = 'skeen.wacrm.note'
_description = 'Nota interna de conversación WACRM'
_order = 'create_date desc, id desc'
conversation_external_id = fields.Char(string='ID conversación WACRM',
required=True, index=True)
contact_name = fields.Char(string='Contacto', help='Sólo para mostrar en la lista')
body = fields.Text(string='Nota', required=True)
author_name = fields.Char(string='Autor')
author_login = fields.Char(string='Autor (usuario)')
resolved = fields.Boolean(string='Atendida', default=False)
resolved_at = fields.Datetime(string='Fecha de atención')
resolved_by = fields.Char(string='Atendida por')
# create_date (heredado) = fecha de la nota
class SkeenWacrmTag(models.Model):
"""Catálogo de etiquetas de conversación (urgente, cotización, etc.)."""
_name = 'skeen.wacrm.tag'
_description = 'Etiqueta de conversación WACRM'
_order = 'sequence, name'
name = fields.Char(string='Nombre', required=True)
color = fields.Char(string='Color', default='#605ca8')
sequence = fields.Integer(string='Orden', default=10)
active = fields.Boolean(string='Activa', default=True)
_sql_constraints = [
('unique_name', 'unique(name)', 'Ya existe una etiqueta con ese nombre.'),
]
class SkeenWacrmConversationTag(models.Model):
"""Asignación etiqueta ↔ conversación, por external_id (sobrevive al sync)."""
_name = 'skeen.wacrm.conversation.tag'
_description = 'Etiqueta asignada a una conversación WACRM'
_order = 'id'
conversation_external_id = fields.Char(string='ID conversación WACRM',
required=True, index=True)
tag_id = fields.Many2one('skeen.wacrm.tag', string='Etiqueta',
required=True, ondelete='cascade', index=True)
assigned_by = fields.Char(string='Asignada por')
_sql_constraints = [
('unique_conv_tag', 'unique(conversation_external_id, tag_id)',
'Esa etiqueta ya está asignada a la conversación.'),
]