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"""
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} + + """ + } + 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 @@ + +- | Mínimo | -Máximo | -Descripción | -
---|---|---|---|
- - | -
- |
-
- |
-
- |
-
- - | -
- |
-
- |
-
- |
-
-
- - | -
- |
-
- |
-
- |
-
- - | -
- |
-
- |
-
- |
-
- - | -
- |
-
- |
-
- |
-
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:
+ +El formato seleccionado para las preguntas escalares es "Estrellas", estas son las ponderaciones:
+ +- | Piso | -Techo | -Descripción | -
---|---|---|---|
- - | -
- |
-
- |
-
- |
-
- - | -
- |
-
- |
-
- |
-
-
- - | -
- |
-
- |
-
- |
-
- - | -
- |
-
- |
-
- |
-
- - | -
- |
-
- |
-
- |
-
En esta sección puedes consultar las preguntas que se mostrarán dentro de la evaluación+
En esta sección puedes asignar y consultar las personas que estarán contempladas para la evaluación
En esta sección puedes consultar las personas externas al sistema que estarán contempladas para la evaluación
En esta sección puedes personalizar los mensajes que se mostrarán dentro de la evaluación y en el correo electrónico
Se revisará a detalle como poder rellenar la plantilla de CSV. Para que pueda crear preguntas conforme a sus necesidades:
+ + +Las diferentes categorías que puede utilizar son:
+Existen 3 tipos de pregunta
+TODAS las preguntas requieren de una categoría.
+Para las preguntas de multiple_choice serán necesarios que tengan opciones las cuales serán delimitadas por comas . Ejemplos: A,B,C,D en este existen 4 opciones
+Las preguntas de tipo escala requieren ponderación. Use ascendente cuando el valor de "SIEMPRE" es el más alto y descendente cuando el valor "NUNCA" es el más alto.
+- Actualmente no tienes objetivos asignados. -
-- Los objetivos que se te asignen aparecerán aquí. -
-+ Actualmente no tienes objetivos asignados. +
++ Los objetivos que se te asignen aparecerán aquí. +
+
Actualmente no hay objetivos creados.
diff --git a/talent360/evaluaciones/views/portada_reporte_template.xml b/talent360/evaluaciones/views/portada_reporte_template.xml
index 9ed7a8830ed0c..2472dab91fe61 100644
--- a/talent360/evaluaciones/views/portada_reporte_template.xml
+++ b/talent360/evaluaciones/views/portada_reporte_template.xml
@@ -2,26 +2,25 @@
Software de Talento Humano
- Reporte
-
-
-
-
-
-
- Fecha:
+
+ Periodo del: hasta el:
- Fecha de expedición:
+
El reporte no contiene información para mostrar + con los filtros de: +
+