259 lines
10 KiB
Python
259 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""API autenticada de encuestas de satisfacción (NPS).
|
|
|
|
Rutas bajo `/skeen/frontend/v1/encuestas/*`. Guard doble `@require_role` +
|
|
`@require_perm` (`encuestas.ver|gestionar|medir`).
|
|
|
|
Con `encuestas.activo=false` (default) no se encola ni se envía nada; el
|
|
reporte simplemente muestra lo que haya (cero en una instancia recién
|
|
instalada).
|
|
"""
|
|
|
|
import json
|
|
from datetime import date, timedelta
|
|
|
|
from odoo import fields, http
|
|
from odoo.http import request
|
|
|
|
from odoo.addons.skeen_whatsapp.controllers.auth import (
|
|
_json, current_user, require_role, require_perm,
|
|
)
|
|
from odoo.addons.skeen_citas.clinic_settings import get_encuestas
|
|
|
|
PREFIX = '/skeen/frontend/v1/encuestas'
|
|
|
|
|
|
def _error(message, status=400):
|
|
return _json({'status': 'error', 'message': message}, status)
|
|
|
|
|
|
def _body():
|
|
try:
|
|
return json.loads(request.httprequest.get_data(as_text=True) or '{}')
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def _rango(kw):
|
|
hoy = date.today()
|
|
start = kw.get('start') or (hoy.replace(day=1)).strftime('%Y-%m-%d')
|
|
end = kw.get('end') or hoy.strftime('%Y-%m-%d')
|
|
return start, end
|
|
|
|
|
|
def _respuesta_dict(r, sucursales=None):
|
|
cita = r.cita_id
|
|
sucursal = ''
|
|
if cita and cita.branch:
|
|
sucursal = (sucursales or {}).get(cita.branch, cita.branch)
|
|
return {
|
|
'id': r.id,
|
|
'name': r.name,
|
|
'partner_id': r.partner_id.id,
|
|
'patient': r.partner_id.name,
|
|
'phone': r.partner_id.whatsapp or r.partner_id.phone or '',
|
|
'nps_puntaje': r.nps_puntaje,
|
|
'nps_categoria': r.nps_categoria or '',
|
|
'comentario': r.nps_comentario or '',
|
|
'llenado_at': r.llenado_at.strftime('%Y-%m-%d %H:%M') if r.llenado_at else None,
|
|
'cita_id': cita.id if cita else None,
|
|
'sucursal': sucursal,
|
|
'doctor': cita.doctor_id.name if cita and cita.doctor_id else '',
|
|
'servicio': cita.servicio_id.name if cita and cita.servicio_id else '',
|
|
}
|
|
|
|
|
|
class SkeenEncuestasController(http.Controller):
|
|
|
|
def _cfg(self):
|
|
return get_encuestas(request.env)
|
|
|
|
def _plantilla(self):
|
|
pid = self._cfg().get('plantilla_id')
|
|
if not pid:
|
|
return request.env['skeen.formulario.plantilla'].sudo().browse()
|
|
p = request.env['skeen.formulario.plantilla'].sudo().browse(int(pid))
|
|
return p if p.exists() else request.env['skeen.formulario.plantilla'].sudo().browse()
|
|
|
|
def _respuestas(self, start, end, solo_nps=False):
|
|
plantilla = self._plantilla()
|
|
if not plantilla:
|
|
return request.env['skeen.formulario.respuesta'].sudo().browse()
|
|
dominio = [
|
|
('plantilla_id', '=', plantilla.id),
|
|
('estado', '=', 'llenado'),
|
|
('llenado_at', '>=', '%s 00:00:00' % start),
|
|
('llenado_at', '<=', '%s 23:59:59' % end),
|
|
]
|
|
if solo_nps:
|
|
dominio.append(('nps_puntaje', '>=', 0))
|
|
return request.env['skeen.formulario.respuesta'].sudo().search(
|
|
dominio, order='llenado_at desc, id desc')
|
|
|
|
def _sucursales(self):
|
|
return {s.code: s.name for s in
|
|
request.env['skeen.sucursal'].sudo().search([])}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Configuración
|
|
# ------------------------------------------------------------------
|
|
@http.route(PREFIX, type='http', auth='none', methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('encuestas.ver')
|
|
def config_encuestas(self, **kw):
|
|
try:
|
|
cfg = self._cfg()
|
|
plantillas = request.env['skeen.formulario.plantilla'].sudo().search(
|
|
[('tipo', '=', 'encuesta')], order='name')
|
|
plantilla = self._plantilla()
|
|
return _json({
|
|
'status': 'success',
|
|
'config': cfg,
|
|
'plantilla': {
|
|
'id': plantilla.id, 'name': plantilla.name,
|
|
'activo': plantilla.activo,
|
|
} if plantilla else None,
|
|
'plantillas': [{
|
|
'id': p.id, 'name': p.name, 'activo': p.activo,
|
|
'tiene_nps': any(c.get('tipo') == 'nps' for c in p.campos_lista()),
|
|
} for p in plantillas],
|
|
})
|
|
except Exception as e:
|
|
return _error(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Reporte de satisfacción
|
|
# ------------------------------------------------------------------
|
|
@http.route(PREFIX + '/reporte', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('encuestas.medir')
|
|
def reporte_satisfaccion(self, **kw):
|
|
try:
|
|
start, end = _rango(kw)
|
|
Respuesta = request.env['skeen.formulario.respuesta'].sudo()
|
|
respuestas = self._respuestas(start, end, solo_nps=True)
|
|
general = Respuesta.nps_de(respuestas)
|
|
sucursales = self._sucursales()
|
|
|
|
# Tendencia por mes (últimos 12 meses, independiente del rango).
|
|
hoy = date.today()
|
|
tendencia = []
|
|
for i in range(11, -1, -1):
|
|
m = (hoy.replace(day=1) - timedelta(days=i * 31)).replace(day=1)
|
|
fin = (m.replace(day=28) + timedelta(days=4)).replace(day=1) - timedelta(days=1)
|
|
del_mes = Respuesta.search([
|
|
('plantilla_id', '=', self._plantilla().id),
|
|
('estado', '=', 'llenado'),
|
|
('nps_puntaje', '>=', 0),
|
|
('llenado_at', '>=', '%s 00:00:00' % m.strftime('%Y-%m-%d')),
|
|
('llenado_at', '<=', '%s 23:59:59' % fin.strftime('%Y-%m-%d')),
|
|
])
|
|
datos = Respuesta.nps_de(del_mes)
|
|
tendencia.append({
|
|
'mes': m.strftime('%Y-%m'),
|
|
'total': datos['total'],
|
|
'nps': datos['nps'],
|
|
'promotores': datos['promotores'],
|
|
'detractores': datos['detractores'],
|
|
})
|
|
|
|
def desglose(etiqueta):
|
|
grupos = {}
|
|
for r in respuestas:
|
|
valor = etiqueta(r) or 'Sin dato'
|
|
grupos.setdefault(valor, Respuesta.browse())
|
|
grupos[valor] |= r
|
|
salida = []
|
|
for valor, resps in grupos.items():
|
|
d = Respuesta.nps_de(resps)
|
|
salida.append({'nombre': valor, **d})
|
|
salida.sort(key=lambda x: (-(x['nps'] if x['nps'] is not None else -999),
|
|
-x['total']))
|
|
return salida
|
|
|
|
return _json({
|
|
'status': 'success',
|
|
'start': start,
|
|
'end': end,
|
|
'general': general,
|
|
'tendencia': tendencia,
|
|
'por_sucursal': desglose(lambda r: (r.cita_id.branch
|
|
and sucursales.get(r.cita_id.branch))
|
|
or (r.cita_id.branch if r.cita_id else '')),
|
|
'por_doctor': desglose(
|
|
lambda r: r.cita_id.doctor_id.name if r.cita_id and r.cita_id.doctor_id else ''),
|
|
'por_servicio': desglose(
|
|
lambda r: r.cita_id.servicio_id.name if r.cita_id and r.cita_id.servicio_id else ''),
|
|
})
|
|
except Exception as e:
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/respuestas', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('encuestas.medir')
|
|
def listar_respuestas(self, **kw):
|
|
try:
|
|
start, end = _rango(kw)
|
|
respuestas = self._respuestas(start, end, solo_nps=False)
|
|
sucursales = self._sucursales()
|
|
return _json({
|
|
'status': 'success',
|
|
'respuestas': [_respuesta_dict(r, sucursales) for r in respuestas[:500]],
|
|
'total': len(respuestas),
|
|
})
|
|
except Exception as e:
|
|
return _error(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Acciones
|
|
# ------------------------------------------------------------------
|
|
@http.route(PREFIX + '/procesar', type='http', auth='none',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('encuestas.gestionar')
|
|
def procesar_encuestas(self, **kw):
|
|
if not self._cfg()['activo']:
|
|
return _error('Las encuestas de salida están apagadas. Actívalas en '
|
|
'Configuración.', 400)
|
|
try:
|
|
resultado = request.env['skeen.encuesta.service'].sudo().ejecutar()
|
|
return _json({'status': 'success', **resultado})
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/respuestas/<int:respuesta_id>/whatsapp', type='http',
|
|
auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('encuestas.medir')
|
|
def responder_whatsapp(self, respuesta_id, **kw):
|
|
"""Responde por WhatsApp al paciente desde el reporte (vía el motor)."""
|
|
respuesta = request.env['skeen.formulario.respuesta'].sudo().browse(respuesta_id)
|
|
if not respuesta.exists():
|
|
return _error('Respuesta no encontrada', 404)
|
|
data = _body()
|
|
mensaje = str(data.get('mensaje') or '').strip()
|
|
if not mensaje:
|
|
return _error('Escribe un mensaje para enviar')
|
|
partner = respuesta.partner_id
|
|
servicio = request.env['skeen.notificacion.service'].sudo()
|
|
try:
|
|
import uuid as _uuid
|
|
notifs = servicio.encolar(
|
|
partner=partner, tipo='manual', canal='whatsapp',
|
|
cuando=fields.Datetime.now(), mensaje=mensaje[:1000],
|
|
clave_unica='encuesta-wa:r%s:%s' % (respuesta.id, _uuid.uuid4().hex[:10]))
|
|
if not notifs:
|
|
return _error('No se pudo enviar. Revisa que las notificaciones '
|
|
'estén activas y el paciente tenga teléfono.', 400)
|
|
cfg = servicio._cfg()
|
|
estados = [servicio._intentar(n, cfg) for n in notifs]
|
|
return _json({'status': 'success',
|
|
'simulado': bool(cfg['modo_prueba']),
|
|
'notificaciones': len(notifs)})
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|