577 lines
24 KiB
Python
577 lines
24 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""API autenticada de campañas y segmentos.
|
|
|
|
Rutas bajo `/skeen/frontend/v1/campanas/*`. Mismo guard doble que el resto del
|
|
frontend: `@require_role` para el rango del rol y `@require_perm` para la acción
|
|
concreta (`campanas.ver|gestionar|enviar|medir`).
|
|
|
|
El módulo nace APAGADO (`campanas.activo=false`): las acciones que envían
|
|
mensajes responden con un aviso claro y no tocan nada. Leer y administrar
|
|
segmentos/campañas sigue disponible para poder configurarlas.
|
|
"""
|
|
|
|
import json
|
|
|
|
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_campanas
|
|
|
|
from ..models.segmento import normalizar_criterios, validar_criterios
|
|
|
|
PREFIX = '/skeen/frontend/v1/campanas'
|
|
|
|
|
|
def _body():
|
|
try:
|
|
return json.loads(request.httprequest.get_data(as_text=True) or '{}')
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def _error(message, status=400):
|
|
return _json({'status': 'error', 'message': message}, status)
|
|
|
|
|
|
def _parse_dt(valor):
|
|
"""Acepta 'YYYY-MM-DD HH:MM', 'YYYY-MM-DDTHH:MM' o vacío."""
|
|
texto = str(valor or '').strip().replace('T', ' ')
|
|
if not texto:
|
|
return False
|
|
if len(texto) == 10:
|
|
texto += ' 00:00:00'
|
|
try:
|
|
return fields.Datetime.to_datetime(texto)
|
|
except Exception:
|
|
raise ValueError('La fecha no es válida (usa AAAA-MM-DD HH:MM)')
|
|
|
|
|
|
def _seg_dict(seg):
|
|
return {
|
|
'id': seg.id,
|
|
'name': seg.name,
|
|
'descripcion': seg.descripcion or '',
|
|
'activo': seg.activo,
|
|
'criterios': seg.criterios_dict(),
|
|
'ultimo_conteo': seg.ultimo_conteo,
|
|
'muestra': seg.muestra_lista(),
|
|
'preview_at': seg.preview_at.strftime('%Y-%m-%d %H:%M') if seg.preview_at else None,
|
|
'creado_por': seg.creado_por or '',
|
|
}
|
|
|
|
|
|
def _camp_dict(c):
|
|
return {
|
|
'id': c.id,
|
|
'name': c.name,
|
|
'descripcion': c.descripcion or '',
|
|
'activo': c.activo,
|
|
'segmento_id': c.segmento_id.id if c.segmento_id else None,
|
|
'segmento': c.segmento_id.name if c.segmento_id else '',
|
|
'canal': c.canal,
|
|
'canales': c.canales_lista(),
|
|
'plantilla': c.plantilla or '',
|
|
'enlace': c.enlace or '',
|
|
'programacion': c.programacion,
|
|
'fecha_programada': c.fecha_programada.strftime('%Y-%m-%d %H:%M')
|
|
if c.fecha_programada else None,
|
|
'hora': c.hora or '',
|
|
'estado': c.estado,
|
|
'ultima_ejecucion': c.ultima_ejecucion.strftime('%Y-%m-%d %H:%M')
|
|
if c.ultima_ejecucion else None,
|
|
'proxima_ejecucion': c.proxima_ejecucion.strftime('%Y-%m-%d %H:%M')
|
|
if c.proxima_ejecucion else None,
|
|
'motivo_pausa': c.motivo_pausa or '',
|
|
'creado_por': c.creado_por or '',
|
|
'notas': c.notas or '',
|
|
}
|
|
|
|
|
|
class SkeenCampanasController(http.Controller):
|
|
|
|
# ------------------------------------------------------------------
|
|
# Utilidades
|
|
# ------------------------------------------------------------------
|
|
def _cfg(self):
|
|
return get_campanas(request.env)
|
|
|
|
def _requiere_activo(self):
|
|
if not self._cfg()['activo']:
|
|
return _error('El módulo de campañas está apagado. Actívalo en '
|
|
'Configuración para enviar.', 400)
|
|
return None
|
|
|
|
def _vals_segmento(self, data, crear):
|
|
vals = {}
|
|
if 'name' in data or crear:
|
|
name = str(data.get('name') or '').strip()
|
|
if not name:
|
|
raise ValueError('El nombre del segmento es obligatorio')
|
|
vals['name'] = name[:120]
|
|
if 'descripcion' in data:
|
|
vals['descripcion'] = str(data.get('descripcion') or '')[:2000]
|
|
if 'activo' in data:
|
|
vals['activo'] = bool(data['activo'])
|
|
if 'criterios' in data or crear:
|
|
vals['criterios'] = json.dumps(
|
|
validar_criterios(data.get('criterios') or {}), ensure_ascii=False)
|
|
return vals
|
|
|
|
def _vals_campana(self, data, crear):
|
|
vals = {}
|
|
if 'name' in data or crear:
|
|
name = str(data.get('name') or '').strip()
|
|
if not name:
|
|
raise ValueError('El nombre de la campaña es obligatorio')
|
|
vals['name'] = name[:200]
|
|
if 'descripcion' in data:
|
|
vals['descripcion'] = str(data.get('descripcion') or '')[:2000]
|
|
if 'activo' in data:
|
|
vals['activo'] = bool(data['activo'])
|
|
if 'segmento_id' in data:
|
|
sid = data.get('segmento_id')
|
|
if sid in (None, '', 0):
|
|
vals['segmento_id'] = False
|
|
else:
|
|
seg = request.env['skeen.segmento'].sudo().browse(int(sid))
|
|
if not seg.exists():
|
|
raise ValueError('El segmento no existe')
|
|
vals['segmento_id'] = seg.id
|
|
if 'canal' in data:
|
|
canal = str(data.get('canal') or '').strip()
|
|
if canal not in ('whatsapp', 'email', 'ambos'):
|
|
raise ValueError('El canal debe ser whatsapp, email o ambos')
|
|
vals['canal'] = canal
|
|
if 'plantilla' in data:
|
|
vals['plantilla'] = str(data.get('plantilla') or '')[:2000]
|
|
if 'enlace' in data:
|
|
vals['enlace'] = str(data.get('enlace') or '').strip()[:500]
|
|
if 'programacion' in data or crear:
|
|
prog = str(data.get('programacion') or 'una_vez').strip()
|
|
if prog not in ('una_vez', 'mensual', 'semanal', 'segmento'):
|
|
raise ValueError('Programación no válida')
|
|
vals['programacion'] = prog
|
|
if 'fecha_programada' in data:
|
|
vals['fecha_programada'] = _parse_dt(data.get('fecha_programada'))
|
|
if 'hora' in data:
|
|
hora = str(data.get('hora') or '').strip()
|
|
partes = hora.split(':')
|
|
if len(partes) != 2:
|
|
raise ValueError('La hora debe tener el formato HH:MM')
|
|
try:
|
|
h, m = int(partes[0]), int(partes[1])
|
|
except (TypeError, ValueError):
|
|
raise ValueError('La hora debe tener el formato HH:MM')
|
|
if not (0 <= h <= 23 and 0 <= m <= 59):
|
|
raise ValueError('La hora debe tener el formato HH:MM')
|
|
vals['hora'] = '%02d:%02d' % (h, m)
|
|
if 'notas' in data:
|
|
vals['notas'] = str(data.get('notas') or '')[:2000]
|
|
return vals
|
|
|
|
# ------------------------------------------------------------------
|
|
# Segmentos
|
|
# ------------------------------------------------------------------
|
|
@http.route(PREFIX + '/segmentos', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.ver')
|
|
def listar_segmentos(self, **kw):
|
|
try:
|
|
incluir_inactivos = str(kw.get('include_inactive') or '') in ('1', 'true', 'True')
|
|
dominio = [] if incluir_inactivos else [('activo', '=', True)]
|
|
segmentos = request.env['skeen.segmento'].sudo().search(dominio, order='name, id')
|
|
return _json({'status': 'success',
|
|
'segmentos': [_seg_dict(s) for s in segmentos],
|
|
'total': len(segmentos)})
|
|
except Exception as e:
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/segmentos/preview', type='http', auth='none',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.ver')
|
|
def preview_criterios(self, **kw):
|
|
"""Vista previa (conteo + muestra) SIN guardar el segmento."""
|
|
try:
|
|
data = _body()
|
|
resultado = request.env['skeen.segmento'].sudo().previsualizar(
|
|
data.get('criterios') or {})
|
|
return _json({'status': 'success', **resultado})
|
|
except Exception as e:
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/segmentos/<int:segmento_id>', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.ver')
|
|
def get_segmento(self, segmento_id, **kw):
|
|
seg = request.env['skeen.segmento'].sudo().browse(segmento_id)
|
|
if not seg.exists():
|
|
return _error('Segmento no encontrado', 404)
|
|
return _json({'status': 'success', 'segmento': _seg_dict(seg)})
|
|
|
|
@http.route(PREFIX + '/segmentos', type='http', auth='none',
|
|
methods=['POST'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.gestionar')
|
|
def crear_segmento(self, **kw):
|
|
data = _body()
|
|
try:
|
|
vals = self._vals_segmento(data, crear=True)
|
|
usuario = current_user(request.env)
|
|
vals['creado_por'] = usuario.name if usuario else ''
|
|
seg = request.env['skeen.segmento'].sudo().create(vals)
|
|
seg.guardar_preview()
|
|
return _json({'status': 'success', 'segmento': _seg_dict(seg)}, 201)
|
|
except ValueError as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 400)
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/segmentos/<int:segmento_id>', type='http', auth='none',
|
|
methods=['PUT', 'OPTIONS'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.gestionar')
|
|
def actualizar_segmento(self, segmento_id, **kw):
|
|
seg = request.env['skeen.segmento'].sudo().browse(segmento_id)
|
|
if not seg.exists():
|
|
return _error('Segmento no encontrado', 404)
|
|
data = _body()
|
|
try:
|
|
seg.write(self._vals_segmento(data, crear=False))
|
|
if 'criterios' in data:
|
|
seg.guardar_preview()
|
|
return _json({'status': 'success', 'segmento': _seg_dict(seg)})
|
|
except ValueError as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 400)
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/segmentos/<int:segmento_id>', type='http', auth='none',
|
|
methods=['DELETE'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.gestionar')
|
|
def borrar_segmento(self, segmento_id, **kw):
|
|
seg = request.env['skeen.segmento'].sudo().browse(segmento_id)
|
|
if not seg.exists():
|
|
return _error('Segmento no encontrado', 404)
|
|
try:
|
|
usadas = request.env['skeen.campana'].sudo().search_count(
|
|
[('segmento_id', '=', seg.id)])
|
|
if usadas:
|
|
seg.write({'activo': False})
|
|
return _json({'status': 'success', 'desactivado': True,
|
|
'message': 'El segmento está en uso: se desactivó '
|
|
'en lugar de borrarse.'})
|
|
seg.unlink()
|
|
return _json({'status': 'success', 'desactivado': False,
|
|
'message': 'Segmento eliminado'})
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/segmentos/<int:segmento_id>/preview', type='http',
|
|
auth='none', methods=['POST', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.ver')
|
|
def recalcular_segmento(self, segmento_id, **kw):
|
|
seg = request.env['skeen.segmento'].sudo().browse(segmento_id)
|
|
if not seg.exists():
|
|
return _error('Segmento no encontrado', 404)
|
|
try:
|
|
seg.guardar_preview()
|
|
return _json({'status': 'success', 'segmento': _seg_dict(seg)})
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Consentimiento
|
|
# ------------------------------------------------------------------
|
|
@http.route(PREFIX + '/consentimiento/<int:partner_id>', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.ver')
|
|
def get_consentimiento(self, partner_id, **kw):
|
|
partner = request.env['res.partner'].sudo().browse(partner_id)
|
|
if not partner.exists():
|
|
return _error('Paciente no encontrado', 404)
|
|
return _json({'status': 'success', 'consentimiento': _consent_dict(partner)})
|
|
|
|
@http.route(PREFIX + '/consentimiento/<int:partner_id>', type='http', auth='none',
|
|
methods=['PUT', 'OPTIONS'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.gestionar')
|
|
def set_consentimiento(self, partner_id, **kw):
|
|
partner = request.env['res.partner'].sudo().browse(partner_id)
|
|
if not partner.exists() or not partner.is_patient:
|
|
return _error('Paciente no encontrado', 404)
|
|
data = _body()
|
|
try:
|
|
usuario = current_user(request.env)
|
|
request.env['res.partner']._marcar_consentimiento(
|
|
partner,
|
|
general=data.get('general'),
|
|
whatsapp=data.get('whatsapp'),
|
|
email=data.get('email'),
|
|
origen=(usuario.name if usuario else '') or str(data.get('origen') or ''))
|
|
return _json({'status': 'success',
|
|
'consentimiento': _consent_dict(partner)})
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Campañas
|
|
# ------------------------------------------------------------------
|
|
@http.route(PREFIX, type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.ver')
|
|
def listar_campanas(self, **kw):
|
|
try:
|
|
dominio = []
|
|
if kw.get('estado'):
|
|
dominio.append(('estado', '=', kw['estado']))
|
|
campanas = request.env['skeen.campana'].sudo().search(
|
|
dominio, order='create_date desc, id desc', limit=500)
|
|
return _json({'status': 'success',
|
|
'campanas': [_camp_dict(c) for c in campanas],
|
|
'total': len(campanas)})
|
|
except Exception as e:
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX, type='http', auth='none', methods=['POST'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.gestionar')
|
|
def crear_campana(self, **kw):
|
|
data = _body()
|
|
try:
|
|
vals = self._vals_campana(data, crear=True)
|
|
usuario = current_user(request.env)
|
|
vals['creado_por'] = usuario.name if usuario else ''
|
|
campana = request.env['skeen.campana'].sudo().create(vals)
|
|
return _json({'status': 'success', 'campana': _camp_dict(campana)}, 201)
|
|
except ValueError as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 400)
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/<int:campana_id>', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.ver')
|
|
def get_campana(self, campana_id, **kw):
|
|
campana = request.env['skeen.campana'].sudo().browse(campana_id)
|
|
if not campana.exists():
|
|
return _error('Campaña no encontrada', 404)
|
|
return _json({'status': 'success', 'campana': _camp_dict(campana)})
|
|
|
|
@http.route(PREFIX + '/<int:campana_id>', type='http', auth='none',
|
|
methods=['PUT', 'OPTIONS'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.gestionar')
|
|
def actualizar_campana(self, campana_id, **kw):
|
|
campana = request.env['skeen.campana'].sudo().browse(campana_id)
|
|
if not campana.exists():
|
|
return _error('Campaña no encontrada', 404)
|
|
data = _body()
|
|
try:
|
|
campana.write(self._vals_campana(data, crear=False))
|
|
return _json({'status': 'success', 'campana': _camp_dict(campana)})
|
|
except ValueError as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 400)
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/<int:campana_id>', type='http', auth='none',
|
|
methods=['DELETE'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.gestionar')
|
|
def borrar_campana(self, campana_id, **kw):
|
|
campana = request.env['skeen.campana'].sudo().browse(campana_id)
|
|
if not campana.exists():
|
|
return _error('Campaña no encontrada', 404)
|
|
try:
|
|
campana.write({'activo': False, 'estado': 'cancelada'})
|
|
return _json({'status': 'success',
|
|
'message': 'La campaña se archivó (cancelada y desactivada).'})
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Acciones
|
|
# ------------------------------------------------------------------
|
|
def _accion(self, campana_id, accion, **kw):
|
|
campana = request.env['skeen.campana'].sudo().browse(campana_id)
|
|
if not campana.exists():
|
|
return _error('Campaña no encontrada', 404)
|
|
try:
|
|
if accion == 'lanzar':
|
|
if not campana.segmento_id:
|
|
return _error('La campaña necesita un segmento para lanzarse')
|
|
if not (campana.plantilla or '').strip():
|
|
return _error('La campaña necesita un mensaje para lanzarse')
|
|
if campana.programacion == 'una_vez' and not campana.fecha_programada:
|
|
campana.fecha_programada = fields.Datetime.now()
|
|
campana.action_lanzar()
|
|
elif accion == 'pausar':
|
|
campana.action_pausar()
|
|
elif accion == 'reanudar':
|
|
campana.action_reanudar()
|
|
elif accion == 'cancelar':
|
|
campana.action_cancelar()
|
|
elif accion == 'borrador':
|
|
campana.action_borrador()
|
|
elif accion == 'procesar':
|
|
servicio = request.env['skeen.campana.service'].sudo()
|
|
resultado = servicio.ejecutar()
|
|
return _json({'status': 'success', **resultado,
|
|
'campana': _camp_dict(campana)})
|
|
return _json({'status': 'success', 'campana': _camp_dict(campana)})
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/<int:campana_id>/<string:accion>', type='http', auth='none',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.enviar')
|
|
def accion_campana(self, campana_id, accion, **kw):
|
|
bloqueo = self._requiere_activo()
|
|
if bloqueo:
|
|
return bloqueo
|
|
if accion not in ('lanzar', 'pausar', 'reanudar', 'cancelar', 'borrador',
|
|
'procesar'):
|
|
return _error('Acción no válida', 404)
|
|
return self._accion(campana_id, accion, **kw)
|
|
|
|
@http.route(PREFIX + '/procesar', type='http', auth='none',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.enviar')
|
|
def procesar_campanas(self, **kw):
|
|
bloqueo = self._requiere_activo()
|
|
if bloqueo:
|
|
return bloqueo
|
|
try:
|
|
resultado = request.env['skeen.campana.service'].sudo().ejecutar()
|
|
return _json({'status': 'success', **resultado})
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/<int:campana_id>/prueba', type='http', auth='none',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
@require_role('recepcion')
|
|
@require_perm('campanas.enviar')
|
|
def prueba_campana(self, campana_id, **kw):
|
|
bloqueo = self._requiere_activo()
|
|
if bloqueo:
|
|
return bloqueo
|
|
campana = request.env['skeen.campana'].sudo().browse(campana_id)
|
|
if not campana.exists():
|
|
return _error('Campaña no encontrada', 404)
|
|
data = _body()
|
|
partner = None
|
|
if data.get('partner_id'):
|
|
partner = request.env['res.partner'].sudo().browse(int(data['partner_id']))
|
|
if not partner.exists() or not partner.is_patient:
|
|
return _error('El paciente indicado no existe', 404)
|
|
try:
|
|
resultado = request.env['skeen.campana.service'].sudo().enviar_prueba(
|
|
campana, partner=partner,
|
|
telefono=str(data.get('telefono') or '').strip(),
|
|
correo=str(data.get('correo') or '').strip())
|
|
return _json({'status': 'success' if resultado.get('ok') else 'error',
|
|
**resultado}, 200 if resultado.get('ok') else 400)
|
|
except Exception as e:
|
|
request.env.cr.rollback()
|
|
return _error(str(e), 500)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Medición
|
|
# ------------------------------------------------------------------
|
|
@http.route(PREFIX + '/<int:campana_id>/metricas', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.medir')
|
|
def metricas_campana(self, campana_id, **kw):
|
|
campana = request.env['skeen.campana'].sudo().browse(campana_id)
|
|
if not campana.exists():
|
|
return _error('Campaña no encontrada', 404)
|
|
try:
|
|
servicio = request.env['skeen.campana.service'].sudo()
|
|
campana_res = campana.with_context(lang=campana.env.user.lang)
|
|
datos = campana_res.metricas(start=kw.get('start'), end=kw.get('end'))
|
|
return _json({'status': 'success', 'metricas': datos})
|
|
except Exception as e:
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/metricas', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.medir')
|
|
def metricas_todas(self, **kw):
|
|
try:
|
|
servicio = request.env['skeen.campana.service'].sudo()
|
|
campanas = request.env['skeen.campana'].sudo().search([], limit=500)
|
|
datos = [campana.metricas(start=kw.get('start'), end=kw.get('end'))
|
|
for campana in campanas]
|
|
total = {
|
|
'alcanzados': sum(d['alcanzados'] for d in datos),
|
|
'enviados': sum(d['enviados'] for d in datos),
|
|
'citas_atribuidas': sum(d['citas_atribuidas'] for d in datos),
|
|
'valor_atribuido': round(sum(d['valor_atribuido'] for d in datos), 2),
|
|
}
|
|
return _json({'status': 'success', 'metricas': datos, 'totales': total})
|
|
except Exception as e:
|
|
return _error(str(e), 500)
|
|
|
|
@http.route(PREFIX + '/<int:campana_id>/envios', type='http', auth='none',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
@require_role('lectura')
|
|
@require_perm('campanas.medir')
|
|
def envios_campana(self, campana_id, **kw):
|
|
campana = request.env['skeen.campana'].sudo().browse(campana_id)
|
|
if not campana.exists():
|
|
return _error('Campaña no encontrada', 404)
|
|
try:
|
|
servicio = request.env['skeen.campana.service'].sudo()
|
|
dominio = [('campana_id', '=', campana.id)]
|
|
if str(kw.get('incluir_pruebas') or '') not in ('1', 'true', 'True'):
|
|
dominio.append(('es_prueba', '=', False))
|
|
notifs = request.env['skeen.notificacion'].sudo().search(
|
|
dominio, order='id desc', limit=500)
|
|
return _json({'status': 'success',
|
|
'envios': [servicio._notif_dict(n) for n in notifs],
|
|
'total': len(notifs)})
|
|
except Exception as e:
|
|
return _error(str(e), 500)
|
|
|
|
|
|
def _consent_dict(partner):
|
|
return {
|
|
'partner_id': partner.id,
|
|
'name': partner.name,
|
|
'general': bool(partner.consentimiento_marketing),
|
|
'whatsapp': bool(partner.consentimiento_whatsapp),
|
|
'email': bool(partner.consentimiento_email),
|
|
'at': partner.consentimiento_at.strftime('%Y-%m-%d %H:%M')
|
|
if partner.consentimiento_at else None,
|
|
'origen': partner.consentimiento_origen or '',
|
|
}
|