diff --git a/talent360/evaluaciones/__manifest__.py b/talent360/evaluaciones/__manifest__.py index 33d8e96115383..8b2c552fa8655 100644 --- a/talent360/evaluaciones/__manifest__.py +++ b/talent360/evaluaciones/__manifest__.py @@ -15,8 +15,11 @@ "views/reporte_nom_035_template.xml", "views/reporte_generico_template.xml", "views/evaluaciones_menus.xml", + "views/importar_preguntas_wizard_views.xml", "views/evaluaciones_responder.xml", "views/wizards_views.xml", + "models/xml/cron_jobs.xml", + "views/registrar_avance_wizard.xml", "data/pregunta.csv", "data/competencia.csv", "data/opcion.csv", diff --git a/talent360/evaluaciones/controllers/evaluaciones.py b/talent360/evaluaciones/controllers/evaluaciones.py index feaff4e8e423d..a18c486035bfb 100644 --- a/talent360/evaluaciones/controllers/evaluaciones.py +++ b/talent360/evaluaciones/controllers/evaluaciones.py @@ -39,6 +39,16 @@ def reporte_controller(self, evaluacion, filtros=None): parametros["filtros"] = filtros + tiene_respuestas = True + + for pregunta in parametros["preguntas"]: + if not pregunta["respuestas"]: + tiene_respuestas = False + break + + if not tiene_respuestas: + return request.render("evaluaciones.encuestas_reporte_no_respuestas", parametros) + if evaluacion.incluir_demograficos: parametros.update(evaluacion.generar_datos_demograficos(filtros)) if evaluacion.tipo == "NOM_035": diff --git a/talent360/evaluaciones/models/__init__.py b/talent360/evaluaciones/models/__init__.py index bce0116dafb71..e38c66dc6f376 100644 --- a/talent360/evaluaciones/models/__init__.py +++ b/talent360/evaluaciones/models/__init__.py @@ -16,3 +16,5 @@ from . import pregunta_escala from . import reporte_resumen from . import usuario_externo +from . import niveles +from . import objetivo_avances diff --git a/talent360/evaluaciones/models/evaluacion.py b/talent360/evaluaciones/models/evaluacion.py index 481196ef17038..a970686aef890 100644 --- a/talent360/evaluaciones/models/evaluacion.py +++ b/talent360/evaluaciones/models/evaluacion.py @@ -2,6 +2,7 @@ from collections import defaultdict, Counter from odoo import exceptions from datetime import timedelta +from odoo.exceptions import ValidationError class Evaluacion(models.Model): @@ -28,11 +29,10 @@ class Evaluacion(models.Model): _description = "Evaluacion de personal" _rec_name = "nombre" nombre = fields.Char(string="Título de la evaluación", required=True) - escalar_format = fields.Selection([ ("numericas", "Numéricas"), ("textuales", "Textuales"), - ("caritas", "Caritas"), + ("caritas", "LIKERT"), ("estrellas", "Estrellas") ], string="Formato para las preguntas escalares", required=True, default="numericas") @@ -44,7 +44,7 @@ class Evaluacion(models.Model): ("generico", "Genérico"), ], required=True, - default="competencia", + default="generico", ) descripcion = fields.Text(string="Descripción") estado = fields.Selection( @@ -89,6 +89,12 @@ class Evaluacion(models.Model): string="Asignados Externos", ) + niveles = fields.One2many( + "niveles", + "evaluacion_id", + string="Niveles", + ) + fecha_inicio = fields.Date(string="Fecha de inicio", required=True) fecha_final = fields.Date(string="Fecha de finalización", required=True) mensaje_bienvenida = fields.Text( @@ -156,13 +162,22 @@ def copiar_preguntas_de_template(self): if not self: + ultimo_id = self.env["evaluacion"].search([], order="id desc", limit=1) + new_evaluation = self.env["evaluacion"].create( { - "nombre": "", + "nombre": str(ultimo_id.id + 1) + " Evaluación Clima", "descripcion": "La evaluación Clima es una herramienta de medición de clima organizacional, cuyo objetivo es conocer la percepción que tienen las personas que laboran en los centros de trabajo, sobre aquellos aspectos sociales que conforman su entorno laboral y que facilitan o dificultan su desempeño.", "tipo": "CLIMA", "fecha_inicio": fields.Date.today(), "fecha_final": fields.Date.today(), + "niveles": [ + (0, 0, {"descripcion_nivel": "Muy malo", "techo": 20, "color": "#ff4747"}), + (0, 0, {"descripcion_nivel": "Malo", "techo": 40, "color": "#ffa446"}), + (0, 0, {"descripcion_nivel": "Regular", "techo": 60, "color": "#ebae14"}), + (0, 0, {"descripcion_nivel": "Bueno", "techo": 80, "color": "#5aaf2b"}), + (0, 0, {"descripcion_nivel": "Muy bueno", "techo": 100, "color": "#2894a7"}), + ], } ) self = new_evaluation @@ -194,9 +209,12 @@ def copiar_preguntas_de_template_nom035(self): """ if not self: + + ultimo_id = self.env["evaluacion"].search([], order="id desc", limit=1) + new_evaluation = self.env["evaluacion"].create( { - "nombre": "", + "nombre": str(ultimo_id.id + 1) + " Evaluación NOM 035", "descripcion": "La NOM 035 tiene como objetivo establecer los elementos para identificar, analizar y prevenir los factores de riesgo psicosocial, así como para promover un entorno organizacional favorable en los centros de trabajo.", "tipo": "NOM_035", "fecha_inicio": fields.Date.today(), @@ -633,9 +651,7 @@ def generar_datos_reporte_clima_action(self, filtros=None): else "Sin departamento" ) elif respuesta.usuario_externo_id: - usuario_externo = self.env["usuario.externo"].browse( - respuesta.usuario_externo_id - ) + usuario_externo = respuesta.usuario_externo_id nombre_departamento = ( usuario_externo.direccion if usuario_externo.direccion @@ -724,9 +740,7 @@ def validar_filtro(self, filtros, respuesta=None, datos_demograficos=None): respuesta.usuario_id ) elif respuesta.usuario_externo_id: - usuario_externo = self.env["usuario.externo"].browse( - respuesta.usuario_externo_id - ) + usuario_externo = respuesta.usuario_externo_id datos_demograficos = self.obtener_datos_demograficos_externos( usuario_externo ) @@ -987,16 +1001,10 @@ def asignar_color_clima(self, valor): :return: El color asignado al valor. """ - if self.techo_verde <= valor <= self.techo_azul: - return "#2894a7" # Azul clarito - elif self.techo_amarillo <= valor <= self.techo_verde: - return "#5aaf2b" # Verde - elif self.techo_naranja <= valor <= self.techo_amarillo: - return "#ebae14" # Amarillo - elif self.techo_rojo <= valor <= self.techo_naranja: - return "#ffa446" # Naranja - else: - return "#ff4747" # Rojo + + for nivel in self.niveles: + if valor <= nivel.techo: + return nivel.color def obtener_dato(self, dato): """ @@ -1162,7 +1170,6 @@ def write(self, vals): ] ) - print(f"Respuestas: {respuestas}") respuestas.unlink() if "usuario_externo_ids" in vals: @@ -1174,13 +1181,18 @@ def write(self, vals): if usuarios_eliminados: respuestas = self.env["respuesta"].search( [ - ("usuario_externo_id", "in", usuarios_eliminados), + ("usuario_externo_id.id", "in", usuarios_eliminados), ("evaluacion_id.id", "=", self.id), ] ) - print(f"Respuestas: {respuestas}") respuestas.unlink() + for record in self: + if record.tipo == "generico" and len(record.pregunta_ids) < 1: + raise exceptions.ValidationError(_("La evaluación debe tener al menos una pregunta.")) + if record.tipo == "generico" and len(record.usuario_ids) < 1: + raise exceptions.ValidationError(_("La evaluación debe tener al menos una persona asignada.")) + return resultado def action_asignar_usuarios_externos(self): @@ -1196,6 +1208,117 @@ def action_asignar_usuarios_externos(self): "view_mode": "form", "target": "new", } + + @api.constrains("niveles") + def checar_techo(self): + """ + Verifica que los valores de la ponderación sean válidos. + Validación 1: Verifica que el valor de la ponderación no sea menor o igual a 0. + Validación 2: Verifica que no haya valores duplicados en la ponderación para la misma evaluación. + Validación 3: Verifica que no haya más de 10 techos. + Validación 4: Verifica que los valores de la ponderación estén en orden ascendente. + Validación 5: Verifica que el valor de las ponderaciones no sean mayores a 100 y que el último sea 100. + + """ + for nivel in self.niveles: + if nivel.techo <= 0: + raise ValidationError( + "El valor de la ponderación no debe ser menor o igual a 0." + ) + + techos = self.niveles.filtered(lambda n: n.id != nivel.id).mapped("techo") + if nivel.techo in techos: + raise ValidationError( + "No puede haber valores duplicados en la ponderación." + ) + + todos_techos = self.niveles.mapped("techo") + if len(todos_techos) > 10: + raise ValidationError( + "No puede haber más de 10 valores de ponderación." + ) + + if todos_techos != sorted(todos_techos): + raise ValidationError( + "Los valores de la ponderación deben estar en orden ascendente." + ) + + if todos_techos[-1] > 100: + raise ValidationError( + "El valor de la ponderación no puede ser mayor a 100." + ) + if todos_techos[-1] != 100: + raise ValidationError("El último valor de la ponderación debe ser 100.") + + + def actualizar_estados_eval(self): + """ + Actualiza el estado de las evaluaciones según la fecha actual. + + - Si la fecha actual está dentro del rango de fechas de inicio y finalización, + se cambia el estado a 'publicado' (Abierta). + - De lo contrario, pasa a 'finalizado' (Cerrada). + + :return: None + """ + + hoy = fields.Date.today() + hora = fields.Datetime.now().strftime("%H:%M:%S") + + # asignar 11:59pm como hora de cierre de evaluaciones + hora_cierre = "23:59:55" + + # Asignar la hora de apertura de las evaluaciones 12:01am + hora_apertura = "00:00:55" + + evaluaciones = self.search([]) + + # Actualizar el estado de las evaluaciones según la fecha y hora actual + for evaluacion in evaluaciones: + if evaluacion.fecha_inicio <= hoy <= evaluacion.fecha_final: + if hoy == evaluacion.fecha_inicio and hora < hora_apertura: + evaluacion.estado = "borrador" + elif hoy == evaluacion.fecha_final and hora > hora_cierre: + evaluacion.estado = "finalizado" + else: + evaluacion.estado = "publicado" + elif evaluacion.fecha_final < hoy: + evaluacion.estado = "finalizado" + elif evaluacion.fecha_inicio > hoy: + evaluacion.estado = "borrador" + + def evaluacion_general_action_form(self): + """ + Ejecuta la acción de redireccionar a la evaluación general y devuelve un diccionario + + Este método utiliza los parámetros necesarios para redireccionar a la evaluación general + + :return: Un diccionario que contiene todos los parámetros necesarios para redireccionar la + a una vista de la evaluación general. + + """ + + ultimo_id = self.env["evaluacion"].search([], order="id desc", limit=1) + + nueva_evaluacion = self.env["evaluacion"].create( + { + "nombre": str(ultimo_id.id + 1) + " Evaluación Genérica", + "tipo": "generico", + "fecha_inicio": fields.Date.today(), + "fecha_final": fields.Date.today(), + } + ) + self = nueva_evaluacion + + return { + "type": "ir.actions.act_window", + "name": "General", + "res_model": "evaluacion", + "view_mode": "form", + "view_id": self.env.ref("evaluaciones.evaluacion_general_view_form").id, + "target": "current", + "res_id": self.id, + } def get_escalar_format(self): """ @@ -1204,3 +1327,36 @@ def get_escalar_format(self): :return: El formato escalar seleccionado para la evaluación. """ return self.escalar_format + + def generar_reporte(self): + """ + Devuelve las fechas de inicio y final que el usuario acordo al realizar la evaluación. + + :return: Las fechas de inicio y final. + """ + return { + + "type": "ir.actions.report", + "report_name": "evaluaciones.reporte_template", + "context": { + "evaluacion_id": self.id, + "fecha_inicio": self.fecha_inicio, + "fecha_final": self.fecha_final, + } + } + + def action_importar_preguntas_clima(self): + """ + Abre la ventana para importar preguntas de clima laboral. + + :return: Una acción para abrir la ventana de importación de preguntas + """ + return { + "name": "Importar preguntas de clima laboral", + "type": "ir.actions.act_window", + "res_model": "importar.preguntas.wizard", + "view_mode": "form", + "target": "new", + } + + diff --git a/talent360/evaluaciones/models/evaluacion_clima.py b/talent360/evaluaciones/models/evaluacion_clima.py index 148b5aca743bc..63d9dd4435f1e 100644 --- a/talent360/evaluaciones/models/evaluacion_clima.py +++ b/talent360/evaluaciones/models/evaluacion_clima.py @@ -100,9 +100,11 @@ def _checar_techos(self): for techo in techos: if techo[1] <= 0: - raise ValidationError(_((f"El nivel {techo[0]} debe ser mayor a 0"))) + raise ValidationError( + _((f"El nivel {techo[0]} debe ser mayor a 0"))) elif techo[1] > 100: - raise ValidationError((f"El nivel {techo[0]} no puede ser mayor a 100")) + raise ValidationError( + (f"El nivel {techo[0]} no puede ser mayor a 100")) for i in range(len(techos) - 1): for j in range(i + 1, len(techos)): @@ -114,3 +116,27 @@ def _checar_techos(self): raise ValidationError( _((f"Los niveles de techo no pueden ser iguales")) ) + + @api.constrains( + "descripcion_rojo", "descripcion_naranja", "descripcion_amarillo", "descripcion_verde", "descripcion_azul" + ) + def _checar_descripciones(self): + """ + Se valida que las descripciones no estén vacías. + """ + descripciones = [ + ("rojo", self.descripcion_rojo), + ("naranja", self.descripcion_naranja), + ("amarillo", self.descripcion_amarillo), + ("verde", self.descripcion_verde), + ("azul", self.descripcion_azul), + ] + + for descripcion in descripciones: + if not descripcion[1]: + raise ValidationError( + _((f"La descripción del nivel {descripcion[0]} no puede estar vacía"))) + + elif len(descripcion[1]) > 50: + raise ValidationError( + _((f"La descripción del nivel {descripcion[0]} no puede tener más de 50 caracteres"))) diff --git a/talent360/evaluaciones/models/niveles.py b/talent360/evaluaciones/models/niveles.py new file mode 100644 index 0000000000000..2146586e2996c --- /dev/null +++ b/talent360/evaluaciones/models/niveles.py @@ -0,0 +1,23 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError + + +class Niveles(models.Model): + """ + Modelo para representar los niveles de semaforización de las evaluaciones. + + :param _name (str): Nombre del modelo en Odoo. + :param _description (str): Descripción del modelo en Odoo. + :param evaluacion_id (Many2one): Relación con el modelo de evaluación. + :param descripcion_nivel (Char): Descripción del nivel. + :param techo (Integer): Ponderación del nivel. + :param color (Char): Color del nivel. + """ + + _name = "niveles" + _description = "Niveles de semaforización" + + evaluacion_id = fields.Many2one("evaluacion", string="Evaluación") + descripcion_nivel = fields.Char(string="Descripción", default="Muy malo") + techo = fields.Integer(string="Ponderación", default=0) + color = fields.Char(string="Color", default="red") \ No newline at end of file diff --git a/talent360/evaluaciones/models/objetivo.py b/talent360/evaluaciones/models/objetivo.py index 2f9d66c535a75..6b3f28611df02 100644 --- a/talent360/evaluaciones/models/objetivo.py +++ b/talent360/evaluaciones/models/objetivo.py @@ -1,6 +1,7 @@ from odoo import _, api, fields, models from odoo.exceptions import ValidationError from datetime import date +from odoo import models class Objetivo(models.Model): @@ -22,13 +23,14 @@ class Objetivo(models.Model): :param estado(fields.Selection): Seleccionar el estado actual del objetivo :param usuario_ids(fields.Many2Many): Arreglo de usuarios asignado a un objetivo :param evaluador(fields.Char): Nombre del evaluador del objetivo + :param avances(fields.One2Many): Avances del objetivo """ _name = "objetivo" _description = "Objetivos de desempeño" _rec_name = "titulo" - titulo = fields.Char(required=True, string="Título") + titulo = fields.Char(required=True, string="Título", help="Título del objetivo") descripcion = fields.Text( required=True, string="Descripción", help="Descripción del objetivo", size="20" ) @@ -36,12 +38,20 @@ class Objetivo(models.Model): [ ("porcentaje", "Porcentaje"), ("monto", "Monto"), + ("otro", "Otro") ], default="porcentaje", required=True, string="Métrica", - help="¿Cómo se medirá el objetivo? Ej. Porcentaje o Monto", + help="¿Cómo se medirá el objetivo? Ej. En porcentaje o en monto", ) + nueva_metrica = fields.Char(string="Nueva Métrica", help="Ingrese una nueva métrica si seleccionó 'Otro'", size=20) + metrica_mostrar = fields.Char( + string="Métrica", compute="_compute_metrica_mostrar", store="True", size=20 + ) + + + tipo = fields.Selection( [ @@ -50,7 +60,7 @@ class Objetivo(models.Model): ], default="puesto", required=True, - help="Tipo de objetivo", + help="Si es individual, el objetivo es tipo 'Del Puesto'. Si es por equipo, el objetivo es tipo 'Estratégico'", ) orden = fields.Selection( @@ -63,12 +73,12 @@ class Objetivo(models.Model): help="Si el objetivo es para lograr una meta, es ascendente, si es para reducir algo, es descendente", ) - peso = fields.Integer(required=True, help="Peso del objetivo en la evaluación") + peso = fields.Integer(required=True, help="Peso del objetivo en la evaluación (no debe incluir decimales).") piso_minimo = fields.Integer( - required=True, string="Piso Mínimo", help="¿Cuál es el mínimo aceptable?" + required=True, string="Piso Mínimo", help="¿Cuál es el resultado mínimo que se espera? (no debe incluir decimales)" ) piso_maximo = fields.Integer( - required=True, string="Piso Máximo", help="¿Cuál es el máximo aceptable?" + required=True, string="Piso Máximo", help="¿Cuál es el resultado que se espera? (no debe incluir decimales)" ) fecha_fin = fields.Date( required=True, @@ -77,6 +87,7 @@ class Objetivo(models.Model): help="Fecha en la que se debe cumplir el objetivo", ) resultado = fields.Integer(store=True) + porcentaje = fields.Float(store=True) estado = fields.Selection( [ ("rojo", "No cumple con las expectativas"), @@ -99,6 +110,8 @@ class Objetivo(models.Model): evaluador = fields.Char() + avances = fields.One2many("objetivo.avances", "objetivo_id", string="Avances") + @api.constrains("piso_minimo", "piso_maximo") def _checar_pisos(self): """ @@ -107,12 +120,14 @@ def _checar_pisos(self): De no ser el caso, el sistema manda un error al usuario. """ for registro in self: - if registro.piso_minimo >= registro.piso_maximo: - raise ValidationError(_("El piso mínimo debe ser menor al piso máximo")) + if registro.orden == "ascendente": + if registro.piso_minimo >= registro.piso_maximo: + raise ValidationError(_("El piso mínimo debe ser menor al piso máximo para objetivos ascendentes")) + else: + if registro.piso_minimo <= registro.piso_maximo: + raise ValidationError(_("El piso mínimo debe ser mayor al piso máximo para objetivos descendentes")) if registro.piso_minimo < 0 or registro.piso_maximo < 0: - raise ValidationError( - _("Los pisos minimos y maximos deben ser mayores a 0") - ) + raise ValidationError(_("Los pisos mínimos y máximos deben ser mayores a 0")) @api.constrains("peso") def _checar_peso(self): @@ -135,15 +150,18 @@ def write(self, vals): De no ser ningún caso, el sistema manda un error al usuario. """ - if "piso_minimo" in vals or "piso_maximo" in vals: + if "piso_minimo" in vals or "piso_maximo" in vals or "orden" in vals: nuevo_piso_minimo = vals.get("piso_minimo", self.piso_minimo) nuevo_piso_maximo = vals.get("piso_maximo", self.piso_maximo) - if nuevo_piso_minimo >= nuevo_piso_maximo: - raise ValidationError(_("El piso mínimo debe ser menor al piso máximo")) + nuevo_orden = vals.get("orden", self.orden) + if nuevo_orden == "ascendente": + if nuevo_piso_minimo >= nuevo_piso_maximo: + raise ValidationError(_("El piso mínimo debe ser menor al piso máximo para objetivos ascendentes")) + else: + if nuevo_piso_minimo <= nuevo_piso_maximo: + raise ValidationError(_("El piso mínimo debe ser mayor al piso máximo para objetivos descendentes")) if nuevo_piso_minimo < 0 or nuevo_piso_maximo < 0: - raise ValidationError( - _("Los pisos mínimos y máximos deben ser mayores a 0") - ) + raise ValidationError(_("Los pisos mínimos y máximos deben ser mayores a 0")) if "peso" in vals: nuevo_peso = vals.get("peso", self.peso) @@ -164,14 +182,18 @@ def _checar_fecha_fin(self): _("La fecha final debe ser mayor a la fecha de hoy") ) - @api.depends("resultado", "piso_maximo") + @api.depends("resultado", "piso_maximo", "piso_minimo", "orden") def _compute_estado(self): """ Método que calcula el estado actual del objetivo dependiendo del resultado """ for registro in self: - if registro.piso_maximo and registro.resultado: - ratio = registro.resultado / registro.piso_maximo + if registro.resultado is None: + registro.estado = "rojo" + continue + + if registro.orden == "ascendente": + ratio = (registro.resultado - registro.piso_minimo) / (registro.piso_maximo - registro.piso_minimo) if registro.piso_maximo != 0 else 0 if 0 <= ratio <= 0.6: registro.estado = "rojo" elif 0.61 <= ratio <= 0.85: @@ -180,6 +202,21 @@ def _compute_estado(self): registro.estado = "verde" elif ratio > 1: registro.estado = "azul" + registro.porcentaje = ratio + else: + ratio = 1 - ((registro.resultado - registro.piso_maximo) / (registro.piso_minimo - registro.piso_maximo)) if registro.piso_minimo != 0 else 0 + if 0 <= ratio <= 0.6: + registro.estado = "rojo" + elif 0.61 <= ratio <= 0.85: + registro.estado = "amarillo" + elif 0.851 <= ratio <= 1: + registro.estado = "verde" + elif ratio > 1: + registro.estado = "azul" + registro.porcentaje = ratio + + if registro.porcentaje < 0: + registro.porcentaje = 0 @api.constrains("usuario_ids") def _checar_usuario_ids(self): @@ -192,4 +229,47 @@ def _checar_usuario_ids(self): if not registro.usuario_ids: raise ValidationError(_("Debe asignar al menos un usuario al objetivo")) - \ No newline at end of file + def registrar_avance_action(self): + """ + Método para llamar la funcionalidad de registro de avances. + """ + return { + "name": "Registrar Avance", + "type": "ir.actions.act_window", + "res_model": "registrar.avance.wizard", + "view_mode": "form", + "target": "new", + } + + @api.onchange("metrica") + def _onchange_metrica(self): + if self.metrica != "otro": + self.nueva_metrica = False + + @api.depends("metrica", "nueva_metrica") + def _compute_metrica_mostrar(self): + """ + Método para calcular la respuesta a mostrar en la vista. + + :return: Respuesta a mostrar en la vista + """ + + for objetivo in self: + if objetivo.metrica == "otro": + metrica_texto = objetivo.nueva_metrica + else: + metrica_texto = objetivo.metrica + + objetivo.metrica_mostrar = metrica_texto + + @api.constrains("metrica", "nueva_metrica") + def _check_nueva_metrica(self): + for record in self: + if record.metrica == "otro" and (not record.nueva_metrica or record.nueva_metrica.strip() == ''): + raise ValidationError(("El campo 'Métrica Personalizada' no puede estar vacío.")) + + @api.model + def create(self, vals): + if vals.get("orden") == "descendente": + vals["resultado"] = vals.get("piso_minimo") + return super(Objetivo, self).create(vals) \ No newline at end of file diff --git a/talent360/evaluaciones/models/objetivo_avances.py b/talent360/evaluaciones/models/objetivo_avances.py new file mode 100644 index 0000000000000..471e2c667a0bb --- /dev/null +++ b/talent360/evaluaciones/models/objetivo_avances.py @@ -0,0 +1,23 @@ +from odoo import fields, models, api, exceptions, _ + +class ObjetivoAvance(models.Model): + """ + Modelo para representar un avance de un objetivo en Odoo + + :param _name(str): Nombre del modelo en Odoo + :param _description (str): Descripción del modelo en Odoo + :param objetivo_id (fields.Many2One): Relación con el objetivo + :param fecha (fields.Date): Fecha del avance + :param avance (fields.Integer): Avance del objetivo + :param comentarios (fields.Text): Comentarios del avance + :param archivos (fields.Many2Many): Archivos adjuntos al avance + """ + + _name = "objetivo.avances" + _description = "Objetivo Avance" + + objetivo_id = fields.Many2one("objetivo", string="Objetivo", required=True, ondelete="cascade") + fecha = fields.Date(string="Fecha", required=True) + avance = fields.Integer(string="Avance", required=True) + comentarios = fields.Text(string="Comentarios") + archivos = fields.Many2many(comodel_name="ir.attachment", string="Archivos") diff --git a/talent360/evaluaciones/models/opcion.py b/talent360/evaluaciones/models/opcion.py index 88c13c70a611c..f161e97cf12bd 100644 --- a/talent360/evaluaciones/models/opcion.py +++ b/talent360/evaluaciones/models/opcion.py @@ -1,4 +1,4 @@ -from odoo import models, fields +from odoo import models, fields, api class Opcion(models.Model): @@ -17,4 +17,4 @@ class Opcion(models.Model): pregunta_id = fields.Many2one("pregunta", string="Pregunta") opcion_texto = fields.Char("Opción", required=True) - valor = fields.Integer(required=True, default=0) + valor = fields.Integer(required=True, default=0) \ No newline at end of file diff --git a/talent360/evaluaciones/models/pregunta.py b/talent360/evaluaciones/models/pregunta.py index b22164ffc8605..dd6a071bde4a1 100644 --- a/talent360/evaluaciones/models/pregunta.py +++ b/talent360/evaluaciones/models/pregunta.py @@ -1,4 +1,5 @@ -from odoo import models, fields +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError class Pregunta(models.Model): @@ -31,7 +32,7 @@ class Pregunta(models.Model): ("open_question", "Abierta"), ("escala", "Escala"), ], - default="multiple_choice", + default="escala", required=True, ) @@ -86,6 +87,14 @@ class Pregunta(models.Model): ], ) + @api.constrains("opcion_ids") + def checkar_opciones(self): + """ + Verifica que haya al menos una opción de respuesta para las preguntas de tipo 'multiple_choice'. + """ + if self.tipo == "multiple_choice" and len(self.opcion_ids) < 2: + raise ValidationError(_("Debe haber al menos dos opciones de respuesta.")) + def _calculate_valor_maximo(self): if self.tipo == "escala": return 4 diff --git a/talent360/evaluaciones/models/respuesta.py b/talent360/evaluaciones/models/respuesta.py index 1174ce4879176..f705b1394e162 100644 --- a/talent360/evaluaciones/models/respuesta.py +++ b/talent360/evaluaciones/models/respuesta.py @@ -19,13 +19,11 @@ class Respuesta(models.Model): _name = "respuesta" _description = "Respuesta a una pregunta" - _rec_name = "respuesta_mostrar" + _rec_name = "pregunta_texto" pregunta_id = fields.Many2one("pregunta", string="Preguntas") usuario_id = fields.Many2one("res.users", string="Usuario") - usuario_externo_id = fields.Integer( - string="Usuario externo", compute="_compute_usuario_externo_id", store=True - ) + usuario_externo_id = fields.Many2one("usuario.externo", string="Usuario externo") evaluacion_id = fields.Many2one("evaluacion", string="Evaluacion") pregunta_texto = fields.Char(related="pregunta_id.pregunta_texto") respuesta_texto = fields.Char("Respuesta") @@ -93,6 +91,13 @@ def guardar_respuesta_action( ) else: + usuario_externo_id = self.env["usuario.evaluacion.rel"].search( + [ + ("token", "=", token), + ("evaluacion_id", "=", evaluacion_id), + ] + ).usuario_externo_id + if escala: resp = self.env["respuesta"].create( { @@ -100,6 +105,7 @@ def guardar_respuesta_action( "token": token, "pregunta_id": pregunta_id, "respuesta_texto": radios, + "usuario_externo_id": usuario_externo_id.id, } ) @@ -110,6 +116,7 @@ def guardar_respuesta_action( "token": token, "pregunta_id": pregunta_id, "respuesta_texto": texto, + "usuario_externo_id": usuario_externo_id.id, } ) @@ -120,6 +127,7 @@ def guardar_respuesta_action( "token": token, "pregunta_id": pregunta_id, "opcion_id": radios, + "usuario_externo_id": usuario_externo_id.id, } ) @@ -158,25 +166,4 @@ def _compute_valor_respuesta(self): elif registro.pregunta_id.tipo == "multiple_choice": registro.valor_respuesta = registro.opcion_id.valor else: - registro.valor_respuesta = 0 - - def _compute_usuario_externo_id(self): - """ - Método para calcular el identificador del usuario externo. - - :return: Identificador del usuario externo - """ - - for registro in self: - usuario_evaluacion_rel = self.env["usuario.evaluacion.rel"].search( - [ - ("token", "=", registro.token), - ("evaluacion_id", "=", registro.evaluacion_id.id), - ] - ) - if usuario_evaluacion_rel.usuario_externo_id: - registro.usuario_externo_id = ( - usuario_evaluacion_rel.usuario_externo_id.id - ) - else: - registro.usuario_externo_id = False + registro.valor_respuesta = 0 \ No newline at end of file diff --git a/talent360/evaluaciones/models/usuario_evaluacion_rel.py b/talent360/evaluaciones/models/usuario_evaluacion_rel.py index a27300dd194a6..2f3d3a7c842e3 100644 --- a/talent360/evaluaciones/models/usuario_evaluacion_rel.py +++ b/talent360/evaluaciones/models/usuario_evaluacion_rel.py @@ -120,21 +120,30 @@ def enviar_evaluacion_action(self, evaluacion_id): usuario.write({"token": token, "contestada": "pendiente"}) evaluacion_url = f"{base_url}/{extension_url}/{evaluacion_id}/{token}" - contenido_adicional = f"""
{evaluacion.contenido_correo}
""" if evaluacion.contenido_correo else "" - mail = { - "subject": "Invitación para completar la evaluación", - "email_from": "talent360@cr-organizacional.com", - "email_to": correo, - "body_html": - f"""

Hola, {nombre},

- {contenido_adicional} -

En {self.env.user.company_id.name} estamos interesados en que contestes la siguiente evaluación, tu participación nos ayudará a mejorar y crecer como organización. La evaluación estará disponible del {evaluacion.fecha_inicio} al {evaluacion.fecha_final}. Puedes comenzar la evaluación haciendo clic en el siguiente enlace: Comenzar {evaluacion.nombre}

-

Gracias por tu colaboración.

-

Atentamente,

-

{self.env.user.name} from {self.env.user.company_id.name}

-

Correo de contacto: {self.env.user.email}

- """, - } + if evaluacion.contenido_correo: + mail = { + "subject": "Invitación para completar la evaluación", + "email_from": "talent360@cr-organizacional.com", + "email_to": correo, + "body_html": f""" + {evaluacion.contenido_correo} +

Comenzar {evaluacion.nombre}

+ """ + } + else: + mail = { + "subject": "Invitación para completar la evaluación", + "email_from": "talent360@cr-organizacional.com", + "email_to": correo, + "body_html": + f"""

Hola, {nombre},

+

En {self.env.user.company_id.name} estamos interesados en que contestes la siguiente evaluación, tu participación nos ayudará a mejorar y crecer como organización. La evaluación estará disponible del {evaluacion.fecha_inicio} al {evaluacion.fecha_final}. Puedes comenzar la evaluación haciendo clic en el siguiente enlace: Comenzar {evaluacion.nombre}

+

Gracias por tu colaboración.

+

Atentamente,

+

{self.env.user.name} from {self.env.user.company_id.name}

+

Correo de contacto: {self.env.user.email}

+ """, + } lista_mails.append(mail) diff --git a/talent360/evaluaciones/models/usuario_objetivo_rel.py b/talent360/evaluaciones/models/usuario_objetivo_rel.py index 04869cd1f4683..358a075e18a91 100644 --- a/talent360/evaluaciones/models/usuario_objetivo_rel.py +++ b/talent360/evaluaciones/models/usuario_objetivo_rel.py @@ -17,6 +17,7 @@ class UsuarioObjetivoRel(models.Model): _name = "usuario.objetivo.rel" _description = "Relación entre objetivos y usuarios" + _rec_name = "titulo" objetivo_id = fields.Many2one("objetivo", string="Objetivos", ondelete="cascade") usuario_id = fields.Many2one("res.users", string="Usuario", ondelete="cascade") @@ -25,7 +26,7 @@ class UsuarioObjetivoRel(models.Model): titulo_corto = fields.Char(compute="_compute_kanban") descripcion = fields.Text(related="objetivo_id.descripcion") descripcion_corta = fields.Char(compute="_compute_kanban") - resultado = fields.Integer(related="objetivo_id.resultado", string="Resultado") + resultado = fields.Float(related="objetivo_id.porcentaje", string="Resultado") def abrir_objetivo_form(self): """ diff --git a/talent360/evaluaciones/models/xml/cron_jobs.xml b/talent360/evaluaciones/models/xml/cron_jobs.xml new file mode 100644 index 0000000000000..0feed96f6ca65 --- /dev/null +++ b/talent360/evaluaciones/models/xml/cron_jobs.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + Actualizar Estados de Evaluaciones + + code + model.actualizar_estados_eval() + + 1 + minutes + -1 + + + + diff --git a/talent360/evaluaciones/security/ir.model.access.csv b/talent360/evaluaciones/security/ir.model.access.csv index df73ba7d8c58b..a3b1a94327ccc 100644 --- a/talent360/evaluaciones/security/ir.model.access.csv +++ b/talent360/evaluaciones/security/ir.model.access.csv @@ -27,4 +27,11 @@ "access_usuario_externo_colaborador_cr","access.usuario.externo","model_usuario_externo","evaluaciones.evaluaciones_colaborador_cr_group_user",1,1,1,1 "access_filtro_wizard_cliente_cr","access.filtro.wizard","model_filtro_wizard","evaluaciones.evaluaciones_cliente_cr_group_user",1,1,1,1 "access_crear_filtros_wizard_cliente_cr","access.crear.filtros.wizard","model_crear_filtros_wizard","evaluaciones.evaluaciones_cliente_cr_group_user",1,1,1,1 -"access_filtro_seleccion_wizard_cliente_cr","access.filtro.seleccion.wizard","model_filtro_seleccion_wizard","evaluaciones.evaluaciones_cliente_cr_group_user",1,1,1,1 \ No newline at end of file +"access_filtro_seleccion_wizard_cliente_cr","access.filtro.seleccion.wizard","model_filtro_seleccion_wizard","evaluaciones.evaluaciones_cliente_cr_group_user",1,1,1,1 +"access_niveles_cliente_cr","access.niveles","model_niveles","evaluaciones.evaluaciones_cliente_cr_group_user",1,1,1,1 +"access_niveles_colaborador_cr","access.niveles","model_niveles","evaluaciones.evaluaciones_colaborador_cr_group_user",1,0,0,0 +"access_registrar_avance_wizard_colaborador_cr","access.registrar.avance.wizard","model_registrar_avance_wizard","evaluaciones.evaluaciones_colaborador_cr_group_user",1,1,1,1 +"access_registrar_avance_wizard_cliente_cr","access.registrar.avance.wizard","model_registrar_avance_wizard","evaluaciones.evaluaciones_cliente_cr_group_user",1,1,1,1 +"access_importar_preguntas_wizard","access_importar_preguntas_wizard","model_importar_preguntas_wizard","evaluaciones.evaluaciones_cliente_cr_group_user",1,1,1,1 +"access_objetivo_avances_cliente_cr","access.objetivo.avances","model_objetivo_avances","evaluaciones.evaluaciones_cliente_cr_group_user",1,1,1,1 +"access_objetivo_avances_colaborador_cr","access.objetivo.avances","model_objetivo_avances","evaluaciones.evaluaciones_colaborador_cr_group_user",1,1,1,1 diff --git a/talent360/evaluaciones/static/csv/plantilla_preguntas_clima.csv b/talent360/evaluaciones/static/csv/plantilla_preguntas_clima.csv new file mode 100644 index 0000000000000..7876449172b67 --- /dev/null +++ b/talent360/evaluaciones/static/csv/plantilla_preguntas_clima.csv @@ -0,0 +1,11 @@ +Pregunta,Tipo,Ponderacion,Categoria,Opciones +Ejemplo para una pregunta de opcion multiple,multiple_choice,,datos_generales,"1,2,3" +Ejemplo de una pregunta abierta,open_question,,reclutamiento_y_seleccion_de_personal, +Ejemplo para una pregunta escala,escala,ascendente,formacion_y_capacitacion, +Ejemplo para una pregunta escala,escala,descendente,permanencia_y_ascenso, +Ejemplo para una pregunta de opcion multiple,multiple_choice,,corresponsabilidad_en_la_vida_laboral_familiar_y_personal,"Siempre, Casi Siempre, A veces, Casi Nunca,Nunca" +Ejemplo para una pregunta de opcion multiple,multiple_choice,,clima_laboral_libre_de_violencia,"A,B,C,D" +Ejemplo de una pregunta abierta,open_question,,acoso_y_hostigamiento, +Ejemplo para una pregunta escala,escala,ascendente,accesibilidad, +Ejemplo de una pregunta abierta,open_question,,respeto_a_la_diversidad, +Ejemplo para una pregunta escala,escala,descendente,condiciones_generales_de_trabajo, \ No newline at end of file diff --git a/talent360/evaluaciones/static/src/img/logo.png b/talent360/evaluaciones/static/src/img/logo.png index 7736fcda01506..c43c279fb5444 100644 Binary files a/talent360/evaluaciones/static/src/img/logo.png and b/talent360/evaluaciones/static/src/img/logo.png differ diff --git a/talent360/evaluaciones/static/src/img/oops.png b/talent360/evaluaciones/static/src/img/oops.png new file mode 100644 index 0000000000000..f14944c1fc407 Binary files /dev/null and b/talent360/evaluaciones/static/src/img/oops.png differ diff --git a/talent360/evaluaciones/static/src/js/handle_response.js b/talent360/evaluaciones/static/src/js/handle_response.js index 29465a34e5788..2e6d1d04866b8 100644 --- a/talent360/evaluaciones/static/src/js/handle_response.js +++ b/talent360/evaluaciones/static/src/js/handle_response.js @@ -26,8 +26,10 @@ function confirmacion() { if (!allFieldsFilled) { alert('Por favor, llena todos los campos requeridos antes de enviar el formulario.'); if (firstUnfilledField) { - firstUnfilledField.scrollIntoView({ behavior: 'smooth' }); - firstUnfilledField.focus(); + firstUnfilledField.scrollIntoView({ behavior: 'smooth', block: 'center' }); + setTimeout(() => { + firstUnfilledField.focus(); + }, 300); // Adjust the delay as needed } return false; } diff --git a/talent360/evaluaciones/static/src/scss/survey_templates_results.scss b/talent360/evaluaciones/static/src/scss/survey_templates_results.scss index 8a2b2624a96fd..07196d4e9eb5e 100644 --- a/talent360/evaluaciones/static/src/scss/survey_templates_results.scss +++ b/talent360/evaluaciones/static/src/scss/survey_templates_results.scss @@ -1,4 +1,25 @@ @media print { + .portada { + display: flex !important; + position: relative; /* Añadir posición relativa */ + + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + border-radius: 10px; + background-color: #f8f9fa; /* Ajusta el color de fondo según tus necesidades */ + min-height: 90vh; /* Para que ocupe toda la altura de la página */ + width: 100%; /* Para que ocupe todo el ancho de la página */ + font-family: Arial, sans-serif; + padding: 0; /* Eliminar cualquier relleno */ + margin: 0; /* Eliminar cualquier margen */ + } + .semaforo-item { + display: flex; + align-items: center; + gap: 10px; + } .page-break { page-break-after: always; @@ -91,7 +112,7 @@ } tbody { tr { - break-inside: avoid; + break-inside: avoid; } tr.d-none { display: table-row !important; @@ -255,6 +276,38 @@ color: #ff4747; } +.clima_color_nulo { + @extend .category_number; + color: #000000; +} + +.clima_color_deficiente { + @extend .category_number; + color: #ff4747; +} + +.clima_color_regular { + @extend .category_number; + color: #fc8803; +} + +.clima_color_marginal { + @extend .category_number; + color: #fcd703; +} + +.clima_color_suficiente { + @extend .category_number; + color: #5aaf2b; +} + +.clima_color_superior { + @extend .category_number; + color: #2894a7; +} + + + .pill { padding: 0.5rem 1rem; border-radius: 2em; @@ -266,50 +319,72 @@ gap: 0.5rem; background-color: #dbdbdb; } - -.color_circle { - aspect-ratio: 1; - width: 22px; - border-radius: 100%; -} - - @page { margin: 0; /* Elimina los márgenes predeterminados de la página */ } + body { margin: 0; /* Elimina los márgenes del cuerpo del documento */ } + .portada { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; + display: none; +} + +.wave-top, .wave-bottom { + position: absolute; + width: 100%; + height: auto; border-radius: 10px; - background-color: #f8f9fa; /* Ajusta el color de fondo según tus necesidades */ - min-height: 85vh; /* Para que ocupe toda la altura de la página */ - width: 100%; /* Para que ocupe todo el ancho de la página */ - font-family: Arial, sans-serif; } + +.wave-top { + top: 0; +} + +.wave-bottom { + bottom: 0; +} + +.company-logo { + height: 250px; + margin-bottom: 10px; /* Añade margen inferior para separar el logo del contenido */ +} + +.main-content { + z-index: 1; /* Asegúrate de que el contenido principal esté encima de las olas */ + padding: 20px; /* Añade padding para separar el contenido del borde */ +} + +.main-content h1, .main-content h2, .main-content p { + margin: 0; /* Eliminar márgenes adicionales */ + margin-bottom: 20px; /* Añade margen inferior para separar los elementos entre sí */ +} + .header { margin-bottom: 20px; } + .icon { margin: 20px 0; } + .icon img { height: 100px; } + .main-content h1 { font-size: 36px; + margin-bottom: 10px; /* Añade margen inferior para separar los encabezados */ } + .main-content h3 { font-size: 24px; + margin-bottom: 10px; /* Añade margen inferior para separar los encabezados */ } + .footer { margin-top: 20px; } -.company-logo { - height: 100px; -} \ No newline at end of file + + diff --git a/talent360/evaluaciones/tests/test_evaluacion.py b/talent360/evaluaciones/tests/test_evaluacion.py index e6f5290076552..eced741cf2793 100644 --- a/talent360/evaluaciones/tests/test_evaluacion.py +++ b/talent360/evaluaciones/tests/test_evaluacion.py @@ -96,3 +96,28 @@ def test_copiar_preguntas_de_template_nom035(self): evaluacion.pregunta_ids = [(6, 0, template.pregunta_ids.ids)] # Verificar que las preguntas se han copiado correctamente self.assertEqual(len(evaluacion.pregunta_ids), 2) + + def test_crear_evaluacion_generica(self): + """ + Prueba crear una evaluación genérica. + + Este método simula la creación de una evaluación genérica sin preguntas predefinidas. + Se crea una evaluación sin preguntas y se verifica que no se haya copiado ninguna pregunta. + + :param nombre: El nombre de la evaluación. + :param estado: El estado de la evaluación (por defecto es 'borrador'). + """ + + evaluacion = self.crear_evaluacion("Evaluación Genérica") + + preguntas = self.env["pregunta"].create( + [ + {"pregunta_texto": "Pregunta 1", "tipo": "open_question"}, + {"pregunta_texto": "Pregunta 2", "tipo": "open_question"}, + ] + ) + + evaluacion.pregunta_ids = [(6, 0, preguntas.ids)] + + # Verificar que no se han copiado preguntas + self.assertEqual(len(evaluacion.pregunta_ids), 2) \ No newline at end of file diff --git a/talent360/evaluaciones/views/crear_evaluacion_clima.xml b/talent360/evaluaciones/views/crear_evaluacion_clima.xml index 93e73ad0f4c33..e6b90ae78c130 100644 --- a/talent360/evaluaciones/views/crear_evaluacion_clima.xml +++ b/talent360/evaluaciones/views/crear_evaluacion_clima.xml @@ -1,14 +1,14 @@ - - + evaluacion_clima.form evaluacion
-

Crear evaluación Clima Laboral

@@ -17,10 +17,10 @@ - + - + @@ -29,18 +29,33 @@ - + + - - - - + + + + + + + + + + + + + + + + + + @@ -58,139 +73,19 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MínimoMáximoDescripción
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
-
-
- - - - - -
+ + + + + + +
- - - + + + @@ -198,4 +93,4 @@
-
\ No newline at end of file + diff --git a/talent360/evaluaciones/views/evaluaciones_menus.xml b/talent360/evaluaciones/views/evaluaciones_menus.xml index 0c9b1cb5ca8dd..85b272dca2bb9 100644 --- a/talent360/evaluaciones/views/evaluaciones_menus.xml +++ b/talent360/evaluaciones/views/evaluaciones_menus.xml @@ -3,8 +3,9 @@ - - + + + diff --git a/talent360/evaluaciones/views/evaluaciones_responder.xml b/talent360/evaluaciones/views/evaluaciones_responder.xml index 652a456af68f8..2f3a369147e7c 100644 --- a/talent360/evaluaciones/views/evaluaciones_responder.xml +++ b/talent360/evaluaciones/views/evaluaciones_responder.xml @@ -26,7 +26,6 @@
-
@@ -187,7 +186,86 @@
-
+ +
+
+ +

El formato seleccionado para las preguntas escalares es Numérico.

+
+ +

El formato seleccionado para las preguntas escalares es Textual.

+
+ +

El formato seleccionado para las preguntas escalares es "LIKERT", estas son las ponderaciones:

+
+
+ Muy enojado + Nunca +
+
+ Casi enojado + Casi nunca +
+
+ Medio + A veces +
+
+ Normal + Casi siempre +
+
+ Contento + Siempre +
+
+
+ +

El formato seleccionado para las preguntas escalares es "Estrellas", estas son las ponderaciones:

+
+
+ 1 estrella + Nunca +
+
+ 2 estrellas + Casi nunca +
+
+ 3 estrellas + A veces +
+
+ 4 estrellas + Casi siempre +
+
+ 5 estrellas + Siempre +
+
+
+
+
+
@@ -207,9 +285,7 @@
- - diff --git a/talent360/evaluaciones/views/evaluaciones_views.xml b/talent360/evaluaciones/views/evaluaciones_views.xml index 913902df04019..84eafc5f7923c 100644 --- a/talent360/evaluaciones/views/evaluaciones_views.xml +++ b/talent360/evaluaciones/views/evaluaciones_views.xml @@ -105,20 +105,24 @@
-

Editar evaluación Clima Laboral

Editar evaluación NOM 035

+

Editar evaluación 360

+

Editar evaluación genérica

+

- + @@ -135,161 +139,80 @@ - - - - - - - - - - - - - - - - + + + @@ -229,5 +239,28 @@ + + \ No newline at end of file diff --git a/talent360/evaluaciones/views/reporte_nom_035_template.xml b/talent360/evaluaciones/views/reporte_nom_035_template.xml index 1c8b975a4bba8..d50172ee11c35 100644 --- a/talent360/evaluaciones/views/reporte_nom_035_template.xml +++ b/talent360/evaluaciones/views/reporte_nom_035_template.xml @@ -46,28 +46,50 @@

Semáforo de riesgos

-
- Riesgo muy alto -
+ +
+ + + + Riesgo muy alto +
-
-
- Riesgo alto -
+ +
+ + + + Riesgo alto +
-
-
- Riesgo medio -
+ +
+ + + + Riesgo medio +
-
-
- Riesgo bajo -
+ +
+ + + + Riesgo bajo +
-
-
- Riesgo nulo + +
+ + + + Riesgo nulo +
diff --git a/talent360/evaluaciones/views/wizards_views.xml b/talent360/evaluaciones/views/wizards_views.xml index c9c161d7e5bf5..05a833c5dfff7 100644 --- a/talent360/evaluaciones/views/wizards_views.xml +++ b/talent360/evaluaciones/views/wizards_views.xml @@ -31,7 +31,7 @@ - +