iteracion 2

This commit is contained in:
Nachou-u
2026-09-07 18:44:32 -03:00
parent a103a47ae6
commit 09a3f6fb37
93 changed files with 7394 additions and 349 deletions
+1
View File
@@ -0,0 +1 @@
# Talent app
+14
View File
@@ -0,0 +1,14 @@
from django.contrib import admin
from .models import SpecialistCandidate, EvaluationRubric7D
@admin.register(SpecialistCandidate)
class SpecialistCandidateAdmin(admin.ModelAdmin):
list_display = ('rut', 'first_name', 'last_name', 'category_code', 'specialty_title', 'seniority', 'trl_level', 'status', 'availability')
search_fields = ('rut', 'first_name', 'last_name', 'specialty_title')
list_filter = ('category_code', 'seniority', 'trl_level', 'status', 'availability')
@admin.register(EvaluationRubric7D)
class EvaluationRubric7DAdmin(admin.ModelAdmin):
list_display = ('candidate', 'evaluator', 'evaluation_date', 'total_weighted_score', 'is_knockout', 'result_status', 'valid_until')
search_fields = ('candidate__rut', 'candidate__first_name', 'candidate__last_name')
list_filter = ('result_status', 'is_knockout', 'evaluation_date')
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TalentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'talent'
verbose_name = 'Cartera de Especialistas y Talento I+D+i'
+66
View File
@@ -0,0 +1,66 @@
from django import forms
from .models import SpecialistCandidate, EvaluationRubric7D
from decimal import Decimal
class SpecialistCandidateForm(forms.ModelForm):
hourly_rate_clp = forms.DecimalField(
label="Tarifa por Hora (CLP)",
required=False,
widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '1000'})
)
monthly_salary_target_clp = forms.DecimalField(
label="Pretensión de Renta Mensual (CLP)",
required=False,
widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '10000'})
)
class Meta:
model = SpecialistCandidate
fields = [
'rut', 'first_name', 'last_name', 'email', 'phone', 'city',
'category_code', 'specialty_title', 'seniority', 'trl_level',
'portfolio_url', 'github_url', 'linkedin_url',
'hourly_rate_clp', 'monthly_salary_target_clp', 'availability',
'status', 'summary'
]
widgets = {
'rut': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Ej: 15.678.910-2', 'pattern': r'^\d{1,2}\.\d{3}\.\d{3}-[\dkK]$', 'title': 'Formato válido: XX.XXX.XXX-X', 'required': True}),
'first_name': forms.TextInput(attrs={'class': 'form-control', 'required': True}),
'last_name': forms.TextInput(attrs={'class': 'form-control', 'required': True}),
'email': forms.EmailInput(attrs={'class': 'form-control', 'required': True}),
'phone': forms.TextInput(attrs={'class': 'form-control', 'pattern': r'^\+?[0-9]{8,15}$', 'title': 'Número de teléfono válido (ej: +56912345678)'}),
'city': forms.HiddenInput(),
'category_code': forms.Select(attrs={'class': 'form-control', 'required': True}),
'specialty_title': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Ej: Ingeniero en Control & Gemelos Digitales', 'required': True}),
'seniority': forms.Select(attrs={'class': 'form-control', 'required': True}),
'trl_level': forms.Select(attrs={'class': 'form-control', 'required': True}),
'portfolio_url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': 'https://...'}),
'github_url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': 'https://github.com/...'}),
'linkedin_url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': 'https://linkedin.com/in/...'}),
'availability': forms.Select(attrs={'class': 'form-control', 'required': True}),
'status': forms.Select(attrs={'class': 'form-control', 'required': True}),
'summary': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Perfil técnico, certificaciones y experiencia en proyectos I+D+i...'}),
}
class EvaluationRubric7DForm(forms.ModelForm):
class Meta:
model = EvaluationRubric7D
fields = [
'candidate', 'evaluation_date',
'dim_case_study', 'dim_portfolio', 'dim_tech_specific', 'dim_cardinal_skills',
'dim_experience', 'dim_principles_safety', 'dim_innovation_rndi',
'feedback'
]
widgets = {
'candidate': forms.Select(attrs={'class': 'form-control', 'id': 'id_candidate', 'required': True}),
'evaluation_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control', 'required': True}),
'dim_case_study': forms.NumberInput(attrs={'class': 'form-control rubro-slider-input', 'min': '0', 'max': '100', 'step': '1', 'id': 'dim_1', 'required': True}),
'dim_portfolio': forms.NumberInput(attrs={'class': 'form-control rubro-slider-input', 'min': '0', 'max': '100', 'step': '1', 'id': 'dim_2', 'required': True}),
'dim_tech_specific': forms.NumberInput(attrs={'class': 'form-control rubro-slider-input', 'min': '0', 'max': '100', 'step': '1', 'id': 'dim_3', 'required': True}),
'dim_cardinal_skills': forms.NumberInput(attrs={'class': 'form-control rubro-slider-input', 'min': '0', 'max': '100', 'step': '1', 'id': 'dim_4', 'required': True}),
'dim_experience': forms.NumberInput(attrs={'class': 'form-control rubro-slider-input', 'min': '0', 'max': '100', 'step': '1', 'id': 'dim_5', 'required': True}),
'dim_principles_safety': forms.NumberInput(attrs={'class': 'form-control rubro-slider-input', 'min': '0', 'max': '100', 'step': '1', 'id': 'dim_6', 'required': True}),
'dim_innovation_rndi': forms.NumberInput(attrs={'class': 'form-control rubro-slider-input', 'min': '0', 'max': '100', 'step': '1', 'id': 'dim_7', 'required': True}),
'feedback': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Comentarios detallados de la evaluación...'}),
}
+76
View File
@@ -0,0 +1,76 @@
# Generated by Django 6.1 on 2026-09-07 02:50
import datetime
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='SpecialistCandidate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('rut', models.CharField(max_length=20, unique=True, verbose_name='RUT Especialista')),
('first_name', models.CharField(max_length=100, verbose_name='Nombres')),
('last_name', models.CharField(max_length=100, verbose_name='Apellidos')),
('email', models.EmailField(max_length=254, verbose_name='Correo Electrónico')),
('phone', models.CharField(blank=True, max_length=30, verbose_name='Teléfono / WhatsApp')),
('city', models.CharField(default='Santiago', max_length=100, verbose_name='Ciudad / Región de Residencia')),
('category_code', models.CharField(choices=[('1', '1. Electricidad de Potencia e Infraestructura'), ('2', '2. Automatización y Control Industrial'), ('3', '3. Domótica e Inmótica (BMS & Climatización)'), ('4', '4. Electrónica y Hardware (PCB / Firmware)'), ('5', '5. Redes, Conectividad e Informática (TI & Ciberseguridad)'), ('6', '6. Tecnologías Inmersivas y Gemelos Digitales (VR/AR/LiDAR)'), ('7', '7. Software, Datos, IA y Cloud')], max_length=10, verbose_name='Partida / Categoría Principal (1 a 7)')),
('specialty_title', models.CharField(max_length=150, verbose_name='Título / Especialidad Técnica Específica')),
('seniority', models.CharField(choices=[('JUNIOR', 'Junior (1-3 años)'), ('SEMI_SENIOR', 'Semi-Senior (3-5 años)'), ('SENIOR', 'Senior (5-8 años)'), ('LEAD_SPECIALIST', 'Lead / Principal Specialist (+8 años)')], default='SEMI_SENIOR', max_length=30, verbose_name='Nivel de Seniority')),
('trl_level', models.IntegerField(choices=[(1, 'TRL 1 - Principios básicos observados'), (2, 'TRL 2 - Concepto tecnológico formulado'), (3, 'TRL 3 - Prueba de concepto experimental'), (4, 'TRL 4 - Validación en laboratorio'), (5, 'TRL 5 - Validación en entorno relevante'), (6, 'TRL 6 - Demostración en entorno relevante'), (7, 'TRL 7 - Demostración de prototipo en entorno operacional'), (8, 'TRL 8 - Sistema completo y calificado'), (9, 'TRL 9 - Sistema probado con éxito en entorno real/industrial')], default=6, verbose_name='Nivel de Madurez Tecnológica Dominado (TRL 1-9)')),
('portfolio_url', models.URLField(blank=True, verbose_name='URL Portafolio / Web Personal')),
('github_url', models.URLField(blank=True, verbose_name='URL GitHub / Repositorio')),
('linkedin_url', models.URLField(blank=True, verbose_name='URL Perfil LinkedIn')),
('hourly_rate_clp', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Tarifa por Hora Pactada (CLP)')),
('monthly_salary_target_clp', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='Pretensión de Renta Mensual (CLP)')),
('availability', models.CharField(choices=[('INMEDIATA', 'Disponibilidad Inmediata Full-Time'), ('DOS_SEMANAS', '2 Semanas (Plazo Aviso)'), ('UN_MES', '1 Mes'), ('FREELANCE_PROYECTO', 'Por Horas / Proyectos Específicos')], default='INMEDIATA', max_length=30, verbose_name='Disponibilidad')),
('status', models.CharField(choices=[('POSTULANTE', 'Postulante Inicial'), ('EN_EVALUACION', 'En Proceso de Evaluación 7D'), ('APROBADO_CARTERA', 'Aprobado en Cartera de Especialistas'), ('EN_RESERVA', 'En Reserva / Banco de Talento'), ('DESCARTADO', 'Descartado / No Apto')], default='POSTULANTE', max_length=30, verbose_name='Estado en Cartera')),
('summary', models.TextField(blank=True, verbose_name='Resumen de Experiencia y Competencias')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Especialista / Candidato',
'verbose_name_plural': 'Cartera de Especialistas',
'ordering': ['category_code', 'last_name', 'first_name'],
},
),
migrations.CreateModel(
name='EvaluationRubric7D',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('evaluation_date', models.DateField(default=datetime.date.today, verbose_name='Fecha de Evaluación')),
('dim_case_study', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='1. Caso Práctico / Prueba Técnica (25% - Knockout < 70)')),
('dim_portfolio', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='2. Portafolio y Proyectos Previos (15%)')),
('dim_tech_specific', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='3. Competencias Técnicas Específicas (15%)')),
('dim_cardinal_skills', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='4. Competencias Cardinales / Blandas (15%)')),
('dim_experience', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='5. Experiencia en Terreno / Industria (10%)')),
('dim_principles_safety', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='6. Alineación con Principios y Seguridad EFINTEL (10%)')),
('dim_innovation_rndi', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='7. Capacidad de Innovación e I+D+i (10%)')),
('is_knockout', models.BooleanField(default=False, verbose_name='Descarte por Criterio Knockout (Prueba < 70)')),
('total_weighted_score', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Puntaje Ponderado Total (0-100)')),
('result_status', models.CharField(choices=[('POSTULANTE', 'Postulante Inicial'), ('EN_EVALUACION', 'En Proceso de Evaluación 7D'), ('APROBADO_CARTERA', 'Aprobado en Cartera de Especialistas'), ('EN_RESERVA', 'En Reserva / Banco de Talento'), ('DESCARTADO', 'Descartado / No Apto')], max_length=30, verbose_name='Dictamen Final')),
('valid_until', models.DateField(verbose_name='Vigencia de la Calificación (12 Meses)')),
('feedback', models.TextField(blank=True, verbose_name='Observaciones, Fortalezas y Brechas Detectadas')),
('created_at', models.DateTimeField(auto_now_add=True)),
('evaluator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Evaluador Líder')),
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='evaluations', to='talent.specialistcandidate', verbose_name='Especialista Candidato')),
],
options={
'verbose_name': 'Evaluación 7D de Especialista',
'verbose_name_plural': 'Evaluaciones Rúbrica 7D',
'ordering': ['-evaluation_date', '-created_at'],
},
),
]
View File
+145
View File
@@ -0,0 +1,145 @@
from django.db import models
from django.contrib.auth.models import User
from decimal import Decimal
import uuid
from datetime import date, timedelta
class SpecialistCategoryEnum(models.TextChoices):
CAT_1_ELECTRICIDAD = '1', '1. Electricidad de Potencia e Infraestructura'
CAT_2_AUTOMATIZACION = '2', '2. Automatización y Control Industrial'
CAT_3_DOMOTICA = '3', '3. Domótica e Inmótica (BMS & Climatización)'
CAT_4_ELECTRONICA = '4', '4. Electrónica y Hardware (PCB / Firmware)'
CAT_5_REDES = '5', '5. Redes, Conectividad e Informática (TI & Ciberseguridad)'
CAT_6_INMERSIVAS = '6', '6. Tecnologías Inmersivas y Gemelos Digitales (VR/AR/LiDAR)'
CAT_7_SOFTWARE_DATOS_IA_CLOUD = '7', '7. Software, Datos, IA y Cloud'
class SeniorityEnum(models.TextChoices):
JUNIOR = 'JUNIOR', 'Junior (1-3 años)'
SEMI_SENIOR = 'SEMI_SENIOR', 'Semi-Senior (3-5 años)'
SENIOR = 'SENIOR', 'Senior (5-8 años)'
LEAD_SPECIALIST = 'LEAD_SPECIALIST', 'Lead / Principal Specialist (+8 años)'
class TRLChoiceEnum(models.IntegerChoices):
TRL_1 = 1, 'TRL 1 - Principios básicos observados'
TRL_2 = 2, 'TRL 2 - Concepto tecnológico formulado'
TRL_3 = 3, 'TRL 3 - Prueba de concepto experimental'
TRL_4 = 4, 'TRL 4 - Validación en laboratorio'
TRL_5 = 5, 'TRL 5 - Validación en entorno relevante'
TRL_6 = 6, 'TRL 6 - Demostración en entorno relevante'
TRL_7 = 7, 'TRL 7 - Demostración de prototipo en entorno operacional'
TRL_8 = 8, 'TRL 8 - Sistema completo y calificado'
TRL_9 = 9, 'TRL 9 - Sistema probado con éxito en entorno real/industrial'
class CandidateStatusEnum(models.TextChoices):
POSTULANTE = 'POSTULANTE', 'Postulante Inicial'
EN_EVALUACION = 'EN_EVALUACION', 'En Proceso de Evaluación 7D'
APROBADO_CARTERA = 'APROBADO_CARTERA', 'Aprobado en Cartera de Especialistas'
EN_RESERVA = 'EN_RESERVA', 'En Reserva / Banco de Talento'
DESCARTADO = 'DESCARTADO', 'Descartado / No Apto'
class AvailabilityEnum(models.TextChoices):
INMEDIATA = 'INMEDIATA', 'Disponibilidad Inmediata Full-Time'
DOS_SEMANAS = 'DOS_SEMANAS', '2 Semanas (Plazo Aviso)'
UN_MES = 'UN_MES', '1 Mes'
FREELANCE_PROYECTO = 'FREELANCE_PROYECTO', 'Por Horas / Proyectos Específicos'
class SpecialistCandidate(models.Model):
"""
Ficha de Especialista y Postulante para Cartera I+D+i EFINTEL.
"""
rut = models.CharField(max_length=20, unique=True, verbose_name="RUT Especialista")
first_name = models.CharField(max_length=100, verbose_name="Nombres")
last_name = models.CharField(max_length=100, verbose_name="Apellidos")
email = models.EmailField(verbose_name="Correo Electrónico")
phone = models.CharField(max_length=30, blank=True, verbose_name="Teléfono / WhatsApp")
city = models.CharField(max_length=100, default="Santiago", verbose_name="Ciudad / Región de Residencia")
# Categorización Técnica
category_code = models.CharField(max_length=10, choices=SpecialistCategoryEnum.choices, verbose_name="Partida / Categoría Principal (1 a 7)")
specialty_title = models.CharField(max_length=150, verbose_name="Título / Especialidad Técnica Específica")
seniority = models.CharField(max_length=30, choices=SeniorityEnum.choices, default=SeniorityEnum.SEMI_SENIOR, verbose_name="Nivel de Seniority")
trl_level = models.IntegerField(choices=TRLChoiceEnum.choices, default=TRLChoiceEnum.TRL_6, verbose_name="Nivel de Madurez Tecnológica Dominado (TRL 1-9)")
# Portafolio y Enlaces
portfolio_url = models.URLField(blank=True, verbose_name="URL Portafolio / Web Personal")
github_url = models.URLField(blank=True, verbose_name="URL GitHub / Repositorio")
linkedin_url = models.URLField(blank=True, verbose_name="URL Perfil LinkedIn")
# Condiciones
hourly_rate_clp = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="Tarifa por Hora Pactada (CLP)")
monthly_salary_target_clp = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name="Pretensión de Renta Mensual (CLP)")
availability = models.CharField(max_length=30, choices=AvailabilityEnum.choices, default=AvailabilityEnum.INMEDIATA, verbose_name="Disponibilidad")
status = models.CharField(max_length=30, choices=CandidateStatusEnum.choices, default=CandidateStatusEnum.POSTULANTE, verbose_name="Estado en Cartera")
summary = models.TextField(blank=True, verbose_name="Resumen de Experiencia y Competencias")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.rut} - {self.first_name} {self.last_name} ({self.get_category_code_display()})"
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
@property
def latest_evaluation(self):
return self.evaluations.order_by('-evaluation_date', '-created_at').first()
class Meta:
verbose_name = "Especialista / Candidato"
verbose_name_plural = "Cartera de Especialistas"
ordering = ['category_code', 'last_name', 'first_name']
class EvaluationRubric7D(models.Model):
"""
Rúbrica de Evaluación Holística en 7 Dimensiones Ponderadas.
Ponderaciones:
1. Caso Práctico / Prueba Técnica: 25% (CRITERIO KNOCKOUT: < 70 Descarte Automático)
2. Portafolio y Proyectos Previos: 15%
3. Competencias Técnicas Específicas: 15%
4. Competencias Cardinales / Blandas: 15%
5. Experiencia Comprobable en Terreno: 10%
6. Alineación con Principios y Seguridad: 10%
7. Capacidad de Innovación e I+D+i: 10%
Vigencia: 12 meses.
"""
candidate = models.ForeignKey(SpecialistCandidate, on_delete=models.CASCADE, related_name='evaluations', verbose_name="Especialista Candidato")
evaluator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="Evaluador Líder")
evaluation_date = models.DateField(default=date.today, verbose_name="Fecha de Evaluación")
# 7 Dimensiones (Escala 0 - 100)
dim_case_study = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="1. Caso Práctico / Prueba Técnica (25% - Knockout < 70)")
dim_portfolio = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="2. Portafolio y Proyectos Previos (15%)")
dim_tech_specific = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="3. Competencias Técnicas Específicas (15%)")
dim_cardinal_skills = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="4. Competencias Cardinales / Blandas (15%)")
dim_experience = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="5. Experiencia en Terreno / Industria (10%)")
dim_principles_safety = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="6. Alineación con Principios y Seguridad EFINTEL (10%)")
dim_innovation_rndi = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="7. Capacidad de Innovación e I+D+i (10%)")
# Flags y Resultado
is_knockout = models.BooleanField(default=False, verbose_name="Descarte por Criterio Knockout (Prueba < 70)")
total_weighted_score = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Puntaje Ponderado Total (0-100)")
result_status = models.CharField(max_length=30, choices=CandidateStatusEnum.choices, verbose_name="Dictamen Final")
valid_until = models.DateField(verbose_name="Vigencia de la Calificación (12 Meses)")
feedback = models.TextField(blank=True, verbose_name="Observaciones, Fortalezas y Brechas Detectadas")
created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
# Auto-set validity to 12 months from evaluation date if not set
if not self.valid_until and self.evaluation_date:
self.valid_until = self.evaluation_date + timedelta(days=365)
super().save(*args, **kwargs)
def __str__(self):
return f"Evaluación 7D {self.candidate.full_name} - Score: {self.total_weighted_score}/100 ({self.get_result_status_display()})"
class Meta:
verbose_name = "Evaluación 7D de Especialista"
verbose_name_plural = "Evaluaciones Rúbrica 7D"
ordering = ['-evaluation_date', '-created_at']
+3
View File
@@ -0,0 +1,3 @@
from .rubric import TalentScoringEngine
__all__ = ['TalentScoringEngine']
+93
View File
@@ -0,0 +1,93 @@
"""
7-Dimension Scoring Engine with Knockout Criterion for EFINTEL Talent Pool.
"""
from decimal import Decimal, ROUND_HALF_UP
from datetime import date, timedelta
from typing import Dict, Any
class TalentScoringEngine:
# 7-Dimension Official Weighting
WEIGHT_CASE_STUDY = Decimal('0.25') # 25% (Knockout if < 70)
WEIGHT_PORTFOLIO = Decimal('0.15') # 15%
WEIGHT_TECH_SPECIFIC = Decimal('0.15') # 15%
WEIGHT_CARDINAL_SKILLS = Decimal('0.15') # 15%
WEIGHT_EXPERIENCE = Decimal('0.10') # 10%
WEIGHT_PRINCIPLES = Decimal('0.10') # 10%
WEIGHT_INNOVATION = Decimal('0.10') # 10%
KNOCKOUT_THRESHOLD = Decimal('70.0')
APPROVAL_THRESHOLD = Decimal('80.0')
RESERVE_THRESHOLD = Decimal('65.0')
VALIDITY_DAYS = 365
@classmethod
def evaluate(
cls,
dim_case_study: Decimal,
dim_portfolio: Decimal,
dim_tech_specific: Decimal,
dim_cardinal_skills: Decimal,
dim_experience: Decimal,
dim_principles_safety: Decimal,
dim_innovation_rndi: Decimal,
evaluation_date: date = None
) -> Dict[str, Any]:
"""
Calculates weighted 7D score, evaluates Knockout condition, determines status and validity.
"""
d1 = Decimal(str(dim_case_study or 0))
d2 = Decimal(str(dim_portfolio or 0))
d3 = Decimal(str(dim_tech_specific or 0))
d4 = Decimal(str(dim_cardinal_skills or 0))
d5 = Decimal(str(dim_experience or 0))
d6 = Decimal(str(dim_principles_safety or 0))
d7 = Decimal(str(dim_innovation_rndi or 0))
# Check Knockout condition on Dimension 1 (Caso Práctico / Prueba Técnica)
is_knockout = d1 < cls.KNOCKOUT_THRESHOLD
# Calculate weighted total
weighted_score = (
(d1 * cls.WEIGHT_CASE_STUDY) +
(d2 * cls.WEIGHT_PORTFOLIO) +
(d3 * cls.WEIGHT_TECH_SPECIFIC) +
(d4 * cls.WEIGHT_CARDINAL_SKILLS) +
(d5 * cls.WEIGHT_EXPERIENCE) +
(d6 * cls.WEIGHT_PRINCIPLES) +
(d7 * cls.WEIGHT_INNOVATION)
).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
# Determine Final Result Status
if is_knockout:
result_status = 'DESCARTADO'
verdict_message = f"Descartado por Criterio Knockout: Caso Práctico ({d1}/100) inferior al mínimo aprobatorio de {cls.KNOCKOUT_THRESHOLD} puntos."
elif weighted_score >= cls.APPROVAL_THRESHOLD:
result_status = 'APROBADO_CARTERA'
verdict_message = f"¡Aprobado en Cartera de Especialistas! Puntaje de {weighted_score}/100 superior al umbral de excelencia ({cls.APPROVAL_THRESHOLD})."
elif weighted_score >= cls.RESERVE_THRESHOLD:
result_status = 'EN_RESERVA'
verdict_message = f"Calificado en Banco de Reserva ({weighted_score}/100). Cumple estándares operacionales para proyectos secundarios o convocatorias de contingencia."
else:
result_status = 'DESCARTADO'
verdict_message = f"Descartado: Puntaje ponderado ({weighted_score}/100) no alcanza el umbral mínimo de corte ({cls.RESERVE_THRESHOLD})."
eval_date = evaluation_date or date.today()
valid_until = eval_date + timedelta(days=cls.VALIDITY_DAYS)
return {
'weighted_score': weighted_score,
'is_knockout': is_knockout,
'result_status': result_status,
'verdict_message': verdict_message,
'valid_until': valid_until,
'breakdown': {
'case_study_weighted': (d1 * cls.WEIGHT_CASE_STUDY).quantize(Decimal('0.01')),
'portfolio_weighted': (d2 * cls.WEIGHT_PORTFOLIO).quantize(Decimal('0.01')),
'tech_specific_weighted': (d3 * cls.WEIGHT_TECH_SPECIFIC).quantize(Decimal('0.01')),
'cardinal_skills_weighted': (d4 * cls.WEIGHT_CARDINAL_SKILLS).quantize(Decimal('0.01')),
'experience_weighted': (d5 * cls.WEIGHT_EXPERIENCE).quantize(Decimal('0.01')),
'principles_weighted': (d6 * cls.WEIGHT_PRINCIPLES).quantize(Decimal('0.01')),
'innovation_weighted': (d7 * cls.WEIGHT_INNOVATION).quantize(Decimal('0.01')),
}
}
+17
View File
@@ -0,0 +1,17 @@
from django.urls import path
from . import views
app_name = 'talent'
urlpatterns = [
path('', views.candidate_list, name='talent_index'),
path('especialistas/', views.candidate_list, name='candidate_list'),
path('especialistas/nuevo/', views.candidate_create, name='candidate_create'),
path('especialistas/<int:pk>/', views.candidate_detail, name='candidate_detail'),
path('especialistas/<int:pk>/editar/', views.candidate_edit, name='candidate_edit'),
# Evaluaciones 7D
path('evaluaciones/nueva/', views.evaluation_create, name='evaluation_create'),
path('especialistas/<int:candidate_pk>/evaluar/', views.evaluation_create, name='evaluation_for_candidate'),
path('evaluaciones/<int:pk>/', views.evaluation_detail, name='evaluation_detail'),
]
+162
View File
@@ -0,0 +1,162 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.http import JsonResponse
from django.db.models import Q, Count, Avg
from decimal import Decimal
from datetime import date
import json
from .models import SpecialistCandidate, EvaluationRubric7D, SpecialistCategoryEnum, SeniorityEnum, CandidateStatusEnum
from .forms import SpecialistCandidateForm, EvaluationRubric7DForm
from .services.rubric import TalentScoringEngine
from expenses.chile_regions import REGIONS_COMMUNES
@login_required
def candidate_list(request):
candidates = SpecialistCandidate.objects.select_related().all().order_by('-created_at')
# Filter params
category = request.GET.get('category')
seniority = request.GET.get('seniority')
trl = request.GET.get('trl')
status = request.GET.get('status')
query = request.GET.get('q')
if category:
candidates = candidates.filter(category_code=category)
if seniority:
candidates = candidates.filter(seniority=seniority)
if trl:
candidates = candidates.filter(trl_level=int(trl))
if status:
candidates = candidates.filter(status=status)
if query:
candidates = candidates.filter(
Q(first_name__icontains=query) |
Q(last_name__icontains=query) |
Q(rut__icontains=query) |
Q(specialty_title__icontains=query) |
Q(summary__icontains=query)
)
# Statistics
stats = {
'total': SpecialistCandidate.objects.count(),
'approved': SpecialistCandidate.objects.filter(status=CandidateStatusEnum.APROBADO_CARTERA).count(),
'reserve': SpecialistCandidate.objects.filter(status=CandidateStatusEnum.EN_RESERVA).count(),
'evaluating': SpecialistCandidate.objects.filter(status=CandidateStatusEnum.EN_EVALUACION).count(),
}
return render(request, 'talent/candidate_list.html', {
'candidates': candidates,
'stats': stats,
'categories': SpecialistCategoryEnum.choices,
'seniorities': SeniorityEnum.choices,
'statuses': CandidateStatusEnum.choices,
'selected_category': category,
'selected_seniority': seniority,
'selected_trl': trl,
'selected_status': status,
'query': query,
})
@login_required
def candidate_create(request):
if request.method == 'POST':
form = SpecialistCandidateForm(request.POST)
if form.is_valid():
cand = form.save()
messages.success(request, f"Especialista {cand.full_name} registrado en cartera.")
return redirect('talent:candidate_detail', pk=cand.pk)
else:
form = SpecialistCandidateForm()
return render(request, 'talent/candidate_form.html', {
'form': form,
'title': 'Registrar Nuevo Especialista I+D+i',
'regions_json': json.dumps(REGIONS_COMMUNES)
})
@login_required
def candidate_edit(request, pk):
cand = get_object_or_404(SpecialistCandidate, pk=pk)
if request.method == 'POST':
form = SpecialistCandidateForm(request.POST, instance=cand)
if form.is_valid():
cand = form.save()
messages.success(request, f"Perfil de {cand.full_name} actualizado.")
return redirect('talent:candidate_detail', pk=cand.pk)
else:
form = SpecialistCandidateForm(instance=cand)
return render(request, 'talent/candidate_form.html', {
'form': form,
'title': f"Editar Especialista: {cand.full_name}",
'regions_json': json.dumps(REGIONS_COMMUNES)
})
@login_required
def candidate_detail(request, pk):
cand = get_object_or_404(SpecialistCandidate, pk=pk)
evaluations = cand.evaluations.select_related('evaluator').order_by('-evaluation_date', '-created_at')
latest_eval = evaluations.first()
return render(request, 'talent/candidate_detail.html', {
'candidate': cand,
'evaluations': evaluations,
'latest_eval': latest_eval,
})
@login_required
def evaluation_create(request, candidate_pk=None):
candidate = get_object_or_404(SpecialistCandidate, pk=candidate_pk) if candidate_pk else None
if request.method == 'POST':
form = EvaluationRubric7DForm(request.POST)
if form.is_valid():
ev = form.save(commit=False)
ev.evaluator = request.user
# Run Scoring Engine
result = TalentScoringEngine.evaluate(
dim_case_study=ev.dim_case_study,
dim_portfolio=ev.dim_portfolio,
dim_tech_specific=ev.dim_tech_specific,
dim_cardinal_skills=ev.dim_cardinal_skills,
dim_experience=ev.dim_experience,
dim_principles_safety=ev.dim_principles_safety,
dim_innovation_rndi=ev.dim_innovation_rndi,
evaluation_date=ev.evaluation_date
)
ev.total_weighted_score = result['weighted_score']
ev.is_knockout = result['is_knockout']
ev.result_status = result['result_status']
ev.valid_until = result['valid_until']
ev.save()
# Sync candidate status
cand_obj = ev.candidate
cand_obj.status = ev.result_status
cand_obj.save(update_fields=['status'])
if ev.is_knockout:
messages.error(request, f"Evaluación finalizada: {result['verdict_message']}")
else:
messages.success(request, f"Evaluación 7D guardada exitosamente. Score: {ev.total_weighted_score}/100 - {result['verdict_message']}")
return redirect('talent:candidate_detail', pk=cand_obj.pk)
else:
initial = {'evaluation_date': date.today()}
if candidate:
initial['candidate'] = candidate
form = EvaluationRubric7DForm(initial=initial)
return render(request, 'talent/evaluation_form.html', {
'form': form,
'candidate': candidate,
})
@login_required
def evaluation_detail(request, pk):
eval_obj = get_object_or_404(EvaluationRubric7D.objects.select_related('candidate', 'evaluator'), pk=pk)
return render(request, 'talent/evaluation_detail.html', {'eval': eval_obj})