feat: Aprimora cálculo de pontuação e extração de dados
- Adiciona campos situacao, anos_completos, anos_consecutivos e retornos na entidade Consultoria para suportar regras documentadas - Implementa mesclagem de períodos sobrepostos para evitar contagem dupla - Melhora componente A com cálculo por área e detecção de retornos - Ajusta componente B com bônus por nota PPG - Refatora componente C com bônus de continuidade e retorno - Implementa componente D com classificação de nível de prêmio (Grande Prêmio, PCT, Interfarma, outros) e pontuação diferenciada - Trata datas inconsistentes (fim < início) como períodos em aberto - Extrai situacaoConsultoria do campo dadosConsultoria.situacaoConsultoria
This commit is contained in:
@@ -34,6 +34,10 @@ class Consultoria:
|
||||
ultimo_evento: datetime
|
||||
vezes_responsavel: int
|
||||
areas: List[str] = field(default_factory=list)
|
||||
situacao: str = "N/A"
|
||||
anos_completos: int = 0
|
||||
anos_consecutivos: int = 0
|
||||
retornos: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -66,6 +70,11 @@ class Consultor:
|
||||
def ativo(self) -> bool:
|
||||
if not self.consultoria:
|
||||
return False
|
||||
situacao = (self.consultoria.situacao or "").lower()
|
||||
if "atividade" in situacao:
|
||||
return True
|
||||
if "inativ" in situacao:
|
||||
return False
|
||||
return self.consultoria.eventos_recentes > 0
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from ..entities.consultor import (
|
||||
@@ -9,9 +9,43 @@ from ..entities.consultor import (
|
||||
Premiacao,
|
||||
)
|
||||
from ..value_objects.pontuacao import ComponentePontuacao, PontuacaoCompleta
|
||||
from ..value_objects.periodo import Periodo
|
||||
|
||||
|
||||
class CalculadorPontuacao:
|
||||
@staticmethod
|
||||
def _mesclar_periodos(periodos: List[Periodo]) -> List[Periodo]:
|
||||
"""
|
||||
Mescla períodos sobrepostos/contíguos para evitar contagem dupla.
|
||||
"""
|
||||
if not periodos:
|
||||
return []
|
||||
|
||||
periodos_ordenados = sorted(periodos, key=lambda p: p.inicio)
|
||||
mesclados: List[Periodo] = []
|
||||
|
||||
for p in periodos_ordenados:
|
||||
if not mesclados:
|
||||
mesclados.append(p)
|
||||
continue
|
||||
|
||||
ultimo = mesclados[-1]
|
||||
ultimo_fim = ultimo.fim or datetime.now()
|
||||
atual_fim = p.fim or datetime.now()
|
||||
|
||||
if p.inicio <= ultimo_fim:
|
||||
novo_fim = max(ultimo_fim, atual_fim)
|
||||
mesclados[-1] = Periodo(inicio=ultimo.inicio, fim=novo_fim if not ultimo.ativo else None)
|
||||
else:
|
||||
mesclados.append(p)
|
||||
|
||||
return mesclados
|
||||
|
||||
@staticmethod
|
||||
def _anos_completos_periodos(periodos: List[Periodo]) -> int:
|
||||
ref = datetime.now()
|
||||
return sum(p.anos_completos(ref) for p in periodos)
|
||||
|
||||
@staticmethod
|
||||
def calcular_componente_a(coordenacoes: List[CoordenacaoCapes]) -> ComponentePontuacao:
|
||||
if not coordenacoes:
|
||||
@@ -38,19 +72,32 @@ class CalculadorPontuacao:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0, retorno=0)
|
||||
|
||||
coord_do_tipo = coord_por_tipo.get(coord_escolhida_tipo, [])
|
||||
anos_total = sum(c.periodo.anos_decorridos for c in coord_do_tipo)
|
||||
ativo = any(c.periodo.ativo for c in coord_do_tipo)
|
||||
coord_por_area = {}
|
||||
for c in coord_do_tipo:
|
||||
area = c.area_avaliacao or "N/A"
|
||||
coord_por_area.setdefault(area, []).append(c.periodo)
|
||||
|
||||
anos_total = 0
|
||||
ativo = False
|
||||
retornos_encontrados = 0
|
||||
|
||||
for _, periodos_area in coord_por_area.items():
|
||||
mesclados = CalculadorPontuacao._mesclar_periodos(periodos_area)
|
||||
anos_total += CalculadorPontuacao._anos_completos_periodos(mesclados)
|
||||
ativo = ativo or any(p.ativo for p in periodos_area)
|
||||
if len(mesclados) > 1:
|
||||
retornos_encontrados += 1
|
||||
|
||||
base = base_map.get(coord_escolhida_tipo, 0)
|
||||
tempo = min(int(anos_total * mult_tempo_map.get(coord_escolhida_tipo, 0)), tempo_max_map.get(coord_escolhida_tipo, 0))
|
||||
|
||||
extras = 0
|
||||
areas_adicionais = [a for c in coord_do_tipo for a in c.areas_adicionais]
|
||||
if areas_adicionais:
|
||||
extras = min(len(set(areas_adicionais)) * 20, 100)
|
||||
areas_distintas = list(coord_por_area.keys())
|
||||
if len(areas_distintas) > 1:
|
||||
extras = min((len(areas_distintas) - 1) * 20, 100)
|
||||
|
||||
bonus = bonus_atual_map.get(coord_escolhida_tipo, 0) if ativo else 0
|
||||
retorno = 20 if len(coord_do_tipo) > 1 else 0
|
||||
retorno = 20 if retornos_encontrados > 0 else 0
|
||||
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus, retorno=retorno)
|
||||
|
||||
@@ -60,33 +107,51 @@ class CalculadorPontuacao:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||
|
||||
base = 70
|
||||
anos_totais = sum(c.periodo.anos_decorridos for c in coordenacoes)
|
||||
anos_totais = sum(c.periodo.anos_completos(datetime.now()) for c in coordenacoes)
|
||||
tempo = min(int(anos_totais * 5), 50)
|
||||
|
||||
programas_distintos = len({c.id_programa for c in coordenacoes})
|
||||
extras = min((programas_distintos - 1) * 20, 40)
|
||||
|
||||
coord_ativa = any(c.periodo.ativo for c in coordenacoes)
|
||||
bonus = 20 if coord_ativa else 0
|
||||
nota_bonus = 0
|
||||
for c in coordenacoes:
|
||||
try:
|
||||
nota_num = float(c.nota_ppg)
|
||||
if nota_num >= 0:
|
||||
nota_bonus = 20
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus)
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=nota_bonus)
|
||||
|
||||
@staticmethod
|
||||
def calcular_componente_c(consultoria: Consultoria) -> ComponentePontuacao:
|
||||
if not consultoria:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||
|
||||
base = 150 if consultoria.eventos_recentes > 0 else 100
|
||||
situacao = (consultoria.situacao or "").lower()
|
||||
base = 150 if "atividade" in situacao else 100
|
||||
|
||||
anos = (datetime.now() - consultoria.primeiro_evento).days / 365.25
|
||||
anos = consultoria.anos_completos if consultoria.anos_completos else int(
|
||||
((datetime.now() - consultoria.primeiro_evento).days) // 365
|
||||
)
|
||||
tempo = min(int(anos * 5), 50)
|
||||
|
||||
extras_eventos = min(consultoria.total_eventos * 2, 20)
|
||||
extras_responsavel = min(consultoria.vezes_responsavel * 5, 25)
|
||||
extras_areas = min((len(consultoria.areas) - 1) * 10, 30) if len(consultoria.areas) > 1 else 0
|
||||
extras = extras_eventos + extras_responsavel + extras_areas
|
||||
continuidade = consultoria.anos_consecutivos
|
||||
if continuidade >= 8:
|
||||
bonus_continuidade = 15
|
||||
elif continuidade >= 5:
|
||||
bonus_continuidade = 10
|
||||
elif continuidade >= 3:
|
||||
bonus_continuidade = 5
|
||||
else:
|
||||
bonus_continuidade = 0
|
||||
|
||||
bonus = 0
|
||||
retorno_bonus = 15 if consultoria.retornos > 0 else 0
|
||||
extras = 0
|
||||
|
||||
bonus = bonus_continuidade + retorno_bonus
|
||||
|
||||
return ComponentePontuacao(base=base, tempo=tempo, extras=extras, bonus=bonus)
|
||||
|
||||
@@ -95,7 +160,11 @@ class CalculadorPontuacao:
|
||||
if not premiacoes:
|
||||
return ComponentePontuacao(base=0, tempo=0, extras=0, bonus=0)
|
||||
|
||||
total_pontos = sum(p.pontos for p in premiacoes)
|
||||
avaliador = [p for p in premiacoes if "avaliador" in (p.tipo or "").lower()]
|
||||
outros = [p for p in premiacoes if p not in avaliador]
|
||||
|
||||
pontos_avaliador = min(sum(p.pontos for p in avaliador), 20)
|
||||
total_pontos = pontos_avaliador + sum(p.pontos for p in outros)
|
||||
total_pontos = min(total_pontos, 180)
|
||||
|
||||
return ComponentePontuacao(base=total_pontos, tempo=0, extras=0, bonus=0)
|
||||
|
||||
@@ -18,6 +18,17 @@ class Periodo:
|
||||
dias = (fim - self.inicio).days
|
||||
return round(dias / 365.25, 1)
|
||||
|
||||
def anos_completos(self, data_referencia: Optional[datetime] = None) -> int:
|
||||
"""
|
||||
Retorna apenas anos completos entre início e fim (ou data de referência).
|
||||
Usado para pontuação que desconsidera frações de ano.
|
||||
"""
|
||||
fim = self.fim or data_referencia or datetime.now()
|
||||
if fim < self.inicio:
|
||||
return 0
|
||||
return int((fim - self.inicio).days // 365)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Se houver fim anterior ao início, o período é tratado como aberto.
|
||||
if self.fim and self.fim < self.inicio:
|
||||
raise ValueError("Data de fim não pode ser anterior à data de início")
|
||||
object.__setattr__(self, "fim", None)
|
||||
|
||||
Reference in New Issue
Block a user