bloque 4: campanas por segmentos con medicion (modulo skeen_campanas: segmentos reutilizables con conteo verificado, campanas por WhatsApp y correo con programacion, prueba previa, limite por minuto, pausa por errores, consentimiento por canal y atribucion de citas) + encuesta al salir con NPS (campo propio, envio automatico y recordatorio, reporte con tendencia y desglose); apagado por defecto
This commit is contained in:
258
odoo-addons/skeen_campanas/controllers/encuestas.py
Normal file
258
odoo-addons/skeen_campanas/controllers/encuestas.py
Normal file
@@ -0,0 +1,258 @@
|
||||
# -*- 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)
|
||||
Reference in New Issue
Block a user