fase 2.5: formularios digitales y consentimientos (modulo skeen_formularios: plantillas con 12 tipos de campo, versionado, respuestas con firma PNG y fotos antes/despues, portal publico con token HMAC y limite de subida, historial por paciente, permisos y notificacion de envio)
This commit is contained in:
139
odoo-addons/skeen_formularios/models/formulario_plantilla.py
Normal file
139
odoo-addons/skeen_formularios/models/formulario_plantilla.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Plantillas de formularios digitales (consentimientos, encuestas, etc.).
|
||||
|
||||
`campos` guarda un JSON con la lista ORDENADA de campos. Se usa Text en lugar
|
||||
de Json para poder validar/limpiar el contenido en el controlador y mantenerlo
|
||||
legible en la BD y en respaldos. Formato de cada campo:
|
||||
|
||||
{clave, etiqueta, tipo, obligatorio, ayuda, opciones[]}
|
||||
|
||||
`momento` es opcional y solo aplica a los campos de tipo `foto`/`archivo`
|
||||
(antes | despues | otro), para armar la galería antes/después del expediente.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
TIPOS_CAMPO = (
|
||||
'texto', 'texto_largo', 'numero', 'fecha', 'seleccion',
|
||||
'multi_seleccion', 'checkbox', 'escala', 'firma', 'foto',
|
||||
'archivo', 'consentimiento',
|
||||
)
|
||||
TIPOS_PLANTILLA = (
|
||||
('consentimiento', 'Consentimiento informado'),
|
||||
('expediente', 'Nota de expediente'),
|
||||
('encuesta', 'Encuesta'),
|
||||
('autorizacion', 'Autorización'),
|
||||
)
|
||||
MOMENTOS = ('antes', 'despues', 'otro')
|
||||
MAX_CAMPOS = 60
|
||||
MAX_OPCIONES = 40
|
||||
MAX_TEXTO = 300
|
||||
|
||||
|
||||
class SkeenFormularioPlantilla(models.Model):
|
||||
_name = 'skeen.formulario.plantilla'
|
||||
_description = 'Plantilla de formulario digital'
|
||||
_order = 'secuencia, name, id'
|
||||
|
||||
name = fields.Char(string='Nombre', required=True)
|
||||
tipo = fields.Selection(TIPOS_PLANTILLA, string='Tipo', required=True,
|
||||
default='consentimiento', index=True)
|
||||
activo = fields.Boolean(string='Activo', default=True, index=True)
|
||||
version = fields.Integer(string='Versión', default=1, readonly=True, copy=False)
|
||||
descripcion = fields.Text(string='Descripción')
|
||||
campos = fields.Text(string='Campos (JSON)', default='[]')
|
||||
texto_legal = fields.Text(string='Texto legal / consentimiento')
|
||||
requiere_firma = fields.Boolean(string='Requiere firma', default=False)
|
||||
requiere_foto = fields.Boolean(string='Requiere foto', default=False)
|
||||
duracion_estimada_min = fields.Integer(string='Duración estimada (min)', default=5)
|
||||
secuencia = fields.Integer(string='Secuencia', default=10)
|
||||
|
||||
sucursales = fields.Many2many(
|
||||
'skeen.sucursal', 'skeen_formulario_plantilla_sucursal_rel',
|
||||
'plantilla_id', 'sucursal_id', string='Sucursales',
|
||||
help='Vacío = todas las sucursales.')
|
||||
servicios = fields.Many2many(
|
||||
'skeen.servicio', 'skeen_formulario_plantilla_servicio_rel',
|
||||
'plantilla_id', 'servicio_id', string='Servicios',
|
||||
help='Sugerir esta plantilla cuando el paciente tenga uno de estos servicios.')
|
||||
|
||||
respuesta_ids = fields.One2many('skeen.formulario.respuesta', 'plantilla_id',
|
||||
string='Respuestas')
|
||||
respuestas_count = fields.Integer(string='# Respuestas', compute='_compute_respuestas_count')
|
||||
|
||||
@api.depends('respuesta_ids')
|
||||
def _compute_respuestas_count(self):
|
||||
for rec in self:
|
||||
rec.respuestas_count = len(rec.respuesta_ids)
|
||||
|
||||
def campos_lista(self):
|
||||
"""Lista de campos normalizada (nunca lanza: JSON inválido -> [])."""
|
||||
self.ensure_one()
|
||||
try:
|
||||
data = json.loads(self.campos or '[]')
|
||||
except (ValueError, TypeError):
|
||||
return []
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
def tiene_respuestas(self):
|
||||
self.ensure_one()
|
||||
return bool(self.respuesta_ids)
|
||||
|
||||
|
||||
def normalizar_campos(raw):
|
||||
"""Valida y limpia la lista de campos de un payload. Lanza ValueError.
|
||||
|
||||
Devuelve siempre una lista nueva (JSON-serializable). Se descartan claves
|
||||
repetidas, tipos desconocidos y opciones fuera de rango.
|
||||
"""
|
||||
if raw is None:
|
||||
raise ValueError('Falta la lista de campos')
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError('Los campos deben ser una lista')
|
||||
if len(raw) > MAX_CAMPOS:
|
||||
raise ValueError('Demasiados campos (máximo %d)' % MAX_CAMPOS)
|
||||
out = []
|
||||
claves = set()
|
||||
for i, campo in enumerate(raw):
|
||||
if not isinstance(campo, dict):
|
||||
raise ValueError('Cada campo debe ser un objeto')
|
||||
clave = str(campo.get('clave') or '').strip()
|
||||
if not clave:
|
||||
raise ValueError('El campo #%d no tiene clave' % (i + 1))
|
||||
if len(clave) > 60:
|
||||
raise ValueError('La clave del campo #%d es demasiado larga' % (i + 1))
|
||||
if clave in claves:
|
||||
raise ValueError('La clave "%s" está repetida' % clave)
|
||||
claves.add(clave)
|
||||
tipo = str(campo.get('tipo') or '').strip()
|
||||
if tipo not in TIPOS_CAMPO:
|
||||
raise ValueError('Tipo de campo no válido: %s' % (tipo or '(vacío)'))
|
||||
etiqueta = str(campo.get('etiqueta') or '').strip() or clave
|
||||
opciones = []
|
||||
if tipo in ('seleccion', 'multi_seleccion'):
|
||||
raw_op = campo.get('opciones') or []
|
||||
if not isinstance(raw_op, list) or not raw_op:
|
||||
raise ValueError('El campo "%s" necesita al menos una opción' % etiqueta)
|
||||
if len(raw_op) > MAX_OPCIONES:
|
||||
raise ValueError('El campo "%s" tiene demasiadas opciones' % etiqueta)
|
||||
for o in raw_op:
|
||||
texto = str(o or '').strip()[:MAX_TEXTO]
|
||||
if texto and texto not in opciones:
|
||||
opciones.append(texto)
|
||||
if not opciones:
|
||||
raise ValueError('El campo "%s" necesita al menos una opción' % etiqueta)
|
||||
clean = {
|
||||
'clave': clave,
|
||||
'etiqueta': etiqueta[:MAX_TEXTO],
|
||||
'tipo': tipo,
|
||||
'obligatorio': bool(campo.get('obligatorio')),
|
||||
'ayuda': str(campo.get('ayuda') or '').strip()[:MAX_TEXTO],
|
||||
'opciones': opciones,
|
||||
}
|
||||
if tipo in ('foto', 'archivo'):
|
||||
momento = str(campo.get('momento') or 'otro').strip()
|
||||
clean['momento'] = momento if momento in MOMENTOS else 'otro'
|
||||
out.append(clean)
|
||||
return out
|
||||
Reference in New Issue
Block a user