# -*- 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