# -*- coding: utf-8 -*- import json from odoo import models, fields, api, _ from odoo.exceptions import ValidationError from ..roles import ROLE_SELECTION, ROLE_RANKS, resolve_agenda_perms, role_rank_map from ..permissions import resolve_role_perms_for_user try: from werkzeug.security import check_password_hash, generate_password_hash except Exception: # pragma: no cover check_password_hash = None generate_password_hash = None class SkeenFrontendUser(models.Model): _name = 'skeen.frontend.user' _description = 'Usuario del Frontend React' _order = 'name, login' _rec_name = 'name' login = fields.Char(string='Usuario', required=True, index=True) name = fields.Char(string='Nombre', required=True) password_hash = fields.Char(string='Hash de contraseña', required=True) role = fields.Selection( selection='_get_role_selection', string='Rol', default='recepcion', required=True) 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') agenda_perms = fields.Text( string='Permisos de Agenda', help="JSON array con las acciones permitidas dentro de la Agenda " "(ver, agendar, editar, eliminar, visitas). Vacío = heredar del rol.") _sql_constraints = [ ('unique_login', 'unique(login)', 'El usuario ya existe.'), ] def set_password(self, plain): self.ensure_one() if not generate_password_hash: raise ValidationError(_('Hash de contraseña no disponible')) if not plain or len(plain) < 8: raise ValidationError(_('La contraseña debe tener al menos 8 caracteres')) self.password_hash = generate_password_hash(plain) def check_password(self, plain): self.ensure_one() if not self.password_hash or not check_password_hash: return False try: return check_password_hash(self.password_hash, plain or '') except Exception: return False def role_rank(self): """Jerarquía simple para permisos: mayor número = más acceso.""" self.ensure_one() return role_rank_map(self.env).get(self.role, ROLE_RANKS.get(self.role, 0)) @api.model def _get_role_selection(self): """Selección de roles configurados (incluye inactivos para no invalidar usuarios históricos); si aún no hay roles usa la lista de siempre.""" from ..roles import role_selection return role_selection(self.env) def _allowed_menus_list(self): """Devuelve la lista de hrefs permitidos o None (usar defaults por rol)""" self.ensure_one() if not self.allowed_menus: return None try: val = json.loads(self.allowed_menus) except Exception: return None return val if isinstance(val, list) and val else None def _agenda_perms_list(self): """Lista explícita de acciones de agenda, o None si el usuario hereda del rol. A diferencia de allowed_menus, una lista vacía SÍ es un valor válido ("desmarcar todo" = solo consultar); por eso solo se devuelve None cuando el campo está vacío o el JSON es inválido. """ self.ensure_one() if not self.agenda_perms: return None try: val = json.loads(self.agenda_perms) except Exception: return None return val if isinstance(val, list) else None def to_public_dict(self): self.ensure_one() return { 'id': self.id, 'login': self.login, 'name': self.name, 'role': self.role, 'role_name': self._role_name(), 'must_change_password': self.must_change_password, 'last_login': self.last_login.strftime('%Y-%m-%d %H:%M') if self.last_login else None, 'allowed_menus': self._allowed_menus_list(), # Lista resuelta (lo que realmente puede hacer) + la lista propia # cruda, para que el editor de usuarios sepa si está heredando. 'agenda_perms': resolve_agenda_perms(self), 'agenda_perms_custom': self._agenda_perms_list(), # Permisos por acción vigentes (rol + overrides), para que el # frontend sepa qué ocultar sin consultar la configuración. 'perms': resolve_role_perms_for_user(self), } def _role_name(self): """Nombre del rol configurado (o el código si no existe).""" self.ensure_one() rol = self.env['skeen.role'].sudo().with_context(active_test=False).search( [('code', '=', self.role)], limit=1) return rol.name if rol else self.role