Files
SKEEN-Proyecto/odoo-addons/skeen_campanas/controllers/encuestas.py

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)