bloque 2: cuenta del cliente (modulo skeen_portal_cliente: acceso con codigo por WhatsApp o enlace por correo, sesion propia separada del personal, mis citas con cancelar y reagendar, pagos, monedero, paquetes, membresias con pago del periodo, formularios, documentos y perfil; invitacion y control de sesiones desde el expediente); apagado por defecto

This commit is contained in:
2026-10-12 00:33:32 +00:00
parent 6a6c2a928b
commit 1bd4cad6a0
26 changed files with 3981 additions and 5 deletions

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import public_portal
from . import staff_portal

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
"""Portal del cliente: endpoints del PERSONAL (invitar, accesos, sesiones).
Bajo `/skeen/frontend/v1/portal`, autenticados con la sesión del personal
(`require_perm`). Un token del portal del cliente NO sirve aquí (no lleva `uid`).
Permiso nuevo: `portal.invitar` (invitar y cerrar sesiones) y `portal.sesiones`
(ver el listado de accesos). Por defecto: admin, gerencia y recepción.
"""
import json
import logging
from odoo import fields, http
from odoo.http import request
from odoo.addons.skeen_whatsapp.controllers.auth import (
_json, require_perm, require_role,
)
from odoo.addons.skeen_whatsapp.controllers import public_booking as _pb
_logger = logging.getLogger(__name__)
PREFIX = '/skeen/frontend/v1/portal'
def _body():
try:
return json.loads(request.httprequest.get_data(as_text=True) or '{}')
except Exception:
return {}
class SkeenPortalStaffController(http.Controller):
"""Gestión del portal del cliente desde la interfaz del personal."""
def _svc(self):
return request.env['skeen.portal.service'].sudo()
# ------------------------------------------------------------------
# Invitar al portal (envía el código o el enlace)
# ------------------------------------------------------------------
@http.route(PREFIX + '/invitar', type='http', auth='none',
methods=['POST', 'OPTIONS'], csrf=False)
@require_role('recepcion')
@require_perm('portal.invitar')
def portal_invitar(self, **kw):
data = _body()
try:
partner = request.env['res.partner'].sudo().browse(
int(data.get('partner_id') or 0))
if not partner.exists() or not partner.is_patient:
return _json({'status': 'error', 'message': 'Paciente no encontrado'}, 404)
svc = self._svc()
cfg = svc.cfg()
if not cfg.get('activo'):
return _json({'status': 'error',
'message': 'El portal del cliente está apagado. '
'Actívalo en Configuración.'}, 403)
canal = str(data.get('canal') or '').strip()
if canal not in ('whatsapp', 'email'):
canal = svc.canal_para(cfg, partner, partner.email or partner.phone or '')
if not svc.destino_disponible(partner, canal):
canal = 'whatsapp' if svc.destino_disponible(partner, 'whatsapp') else 'email'
if not svc.destino_disponible(partner, canal):
return _json({'status': 'error',
'message': 'El paciente no tiene teléfono ni correo.'}, 400)
usuario = getattr(request, 'frontend_user', None)
acceso = svc.acceso_de(partner, crear=True, metodo=cfg.get('metodo'),
invitado_por=usuario.login if usuario else '')
acceso.write({'activo': True, 'invitado_at': fields.Datetime.now()})
salida = {'status': 'success', 'ok': True, 'canal': canal,
'modo_prueba': svc.modo_prueba(),
'acceso': svc.estado_paciente(partner)}
if canal == 'whatsapp':
codigo = svc.enviar_codigo(partner, ip=_pb._client_ip())
if svc.modo_prueba():
salida['codigo_prueba'] = codigo
else:
url = svc.enviar_enlace(partner, ip=_pb._client_ip())
if svc.modo_prueba():
salida['enlace_prueba'] = url
return _json(salida)
except Exception as exc:
request.env.cr.rollback()
_logger.exception('Error invitando al portal: %s', exc)
return _json({'status': 'error',
'message': 'No se pudo invitar al portal.'}, 500)
# ------------------------------------------------------------------
# Estado del acceso de un paciente (ficha de expediente)
# ------------------------------------------------------------------
@http.route(PREFIX + '/paciente/<int:partner_id>', type='http', auth='none',
methods=['GET', 'OPTIONS'], csrf=False)
@require_role('lectura')
@require_perm('portal.invitar')
def portal_paciente_estado(self, partner_id, **kw):
try:
partner = request.env['res.partner'].sudo().browse(partner_id)
if not partner.exists() or not partner.is_patient:
return _json({'status': 'error', 'message': 'Paciente no encontrado'}, 404)
return _json({'status': 'success',
'activo': self._svc().activo(),
'estado': self._svc().estado_paciente(partner)})
except Exception as exc:
_logger.exception('Error consultando acceso del portal: %s', exc)
return _json({'status': 'error', 'message': 'No se pudo consultar.'}, 500)
# ------------------------------------------------------------------
# Listado de accesos activos
# ------------------------------------------------------------------
@http.route(PREFIX + '/accesos', type='http', auth='none',
methods=['GET', 'OPTIONS'], csrf=False)
@require_role('lectura')
@require_perm('portal.sesiones')
def portal_accesos(self, **kw):
try:
svc = self._svc()
return _json({'status': 'success',
'activo': svc.activo(),
'accesos': svc.lista_accesos()})
except Exception as exc:
_logger.exception('Error listando accesos del portal: %s', exc)
return _json({'status': 'error', 'message': 'No se pudo consultar.'}, 500)
# ------------------------------------------------------------------
# Cerrar sesiones de un paciente
# ------------------------------------------------------------------
@http.route(PREFIX + '/sesiones/cerrar', type='http', auth='none',
methods=['POST', 'OPTIONS'], csrf=False)
@require_role('recepcion')
@require_perm('portal.invitar')
def portal_cerrar_sesiones(self, **kw):
data = _body()
try:
partner = request.env['res.partner'].sudo().browse(
int(data.get('partner_id') or 0))
if not partner.exists() or not partner.is_patient:
return _json({'status': 'error', 'message': 'Paciente no encontrado'}, 404)
svc = self._svc()
acceso = svc.acceso_de(partner, crear=False)
if not acceso:
return _json({'status': 'error',
'message': 'El paciente no tiene acceso al portal.'}, 404)
acceso.action_cerrar_sesiones()
return _json({'status': 'success', 'ok': True,
'acceso': svc.estado_paciente(partner)})
except Exception as exc:
request.env.cr.rollback()
_logger.exception('Error cerrando sesiones del portal: %s', exc)
return _json({'status': 'error', 'message': 'No se pudo cerrar sesiones.'}, 500)
# ------------------------------------------------------------------
# Activar / desactivar el acceso de un paciente
# ------------------------------------------------------------------
@http.route(PREFIX + '/acceso/<int:partner_id>/<string:accion>', type='http',
auth='none', methods=['POST', 'OPTIONS'], csrf=False)
@require_role('recepcion')
@require_perm('portal.invitar')
def portal_acceso_estado(self, partner_id, accion, **kw):
try:
partner = request.env['res.partner'].sudo().browse(partner_id)
if not partner.exists() or not partner.is_patient:
return _json({'status': 'error', 'message': 'Paciente no encontrado'}, 404)
if accion not in ('activar', 'desactivar'):
return _json({'status': 'error', 'message': 'Acción no válida'}, 400)
svc = self._svc()
acceso = svc.acceso_de(partner, crear=(accion == 'activar'))
if not acceso:
return _json({'status': 'error',
'message': 'El paciente no tiene acceso al portal.'}, 404)
activo = accion == 'activar'
acceso.write({'activo': activo})
if not activo:
acceso.action_cerrar_sesiones()
return _json({'status': 'success', 'ok': True,
'acceso': svc.estado_paciente(partner)})
except Exception as exc:
request.env.cr.rollback()
_logger.exception('Error cambiando el acceso al portal: %s', exc)
return _json({'status': 'error', 'message': 'No se pudo actualizar.'}, 500)