152 lines
5.8 KiB
Python
152 lines
5.8 KiB
Python
"""Orquestração do Indicador de Desempenho: lê as duas planilhas, deriva os
|
|
papéis/tipos por empresa e pré-calcula os 3 critérios automáticos por
|
|
colaborador. Só calcula em memória (dataclasses), sem tocar no ORM — mesmo
|
|
padrão de `portal_api.planos_saude.pipeline`; quem persiste é a view."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from decimal import Decimal
|
|
|
|
from .entregas import calcula_percentual_entrega
|
|
from .leiaute import (
|
|
SERVICO_CONCILIACAO_FINANCEIRA,
|
|
SERVICO_LIBERACAO_FISCAL,
|
|
SERVICOS_BALANCETE,
|
|
le_honorarios,
|
|
le_servicos_tareffa,
|
|
)
|
|
from .tipos import PapelPorEmpresa, deriva_tipos_por_empresa
|
|
|
|
CALCULO_BALANCETE = "balancete"
|
|
CALCULO_LIBERACAO_FISCAL = "liberacao_fiscal"
|
|
CALCULO_CONCILIACAO = "conciliacao"
|
|
|
|
SERVICOS_POR_CALCULO_AUTOMATICO = {
|
|
CALCULO_BALANCETE: SERVICOS_BALANCETE,
|
|
CALCULO_LIBERACAO_FISCAL: {SERVICO_LIBERACAO_FISCAL},
|
|
CALCULO_CONCILIACAO: {SERVICO_CONCILIACAO_FINANCEIRA},
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class CriterioAutomaticoConfig:
|
|
criterio_id: int
|
|
tipo_calculo: str # "balancete" | "liberacao_fiscal" | "conciliacao"
|
|
limiar_percentual: Decimal
|
|
|
|
|
|
@dataclass
|
|
class RespostaAutomatica:
|
|
criterio_id: int
|
|
valor: str # "SIM" | "NAO" | "NAO_FAZ"
|
|
percentual_calculado: Decimal | None
|
|
|
|
|
|
@dataclass
|
|
class EmpresaCalculada:
|
|
codigo_empresa: str
|
|
nome_empresa: str
|
|
honorario: Decimal
|
|
honorario_nao_encontrado: bool
|
|
tipo: str
|
|
|
|
|
|
@dataclass
|
|
class ColaboradorCalculado:
|
|
nome: str
|
|
gerente: str
|
|
departamento_id: int | None = None
|
|
empresas: list[EmpresaCalculada] = field(default_factory=list)
|
|
respostas_automaticas: list[RespostaAutomatica] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class ResultadoApuracao:
|
|
colaboradores: list[ColaboradorCalculado]
|
|
avisos: list[str]
|
|
|
|
|
|
def processa_apuracao(
|
|
caminho_tareffa: str,
|
|
caminho_honorarios: str,
|
|
gerente_departamento: dict[str, int],
|
|
criterios_automaticos_por_departamento: dict[int, list[CriterioAutomaticoConfig]],
|
|
) -> ResultadoApuracao:
|
|
"""`gerente_departamento` ({nome do gerente: id do IndicadorDepartamento})
|
|
e `criterios_automaticos_por_departamento` ({id do departamento: lista de
|
|
critérios automáticos daquele departamento}) vêm prontos de fora (ORM
|
|
fica só na view) — cada colaborador é resolvido pro departamento do seu
|
|
`gerente` antes de decidir quais critérios automáticos calcular pra ele,
|
|
já que cada departamento pode ter critérios/pesos/limiares diferentes.
|
|
Colaborador cujo gerente não está em `gerente_departamento` fica com
|
|
`departamento_id=None` e sem nenhum critério automático calculado (vira
|
|
aviso, ver `avisos` abaixo)."""
|
|
linhas = le_servicos_tareffa(caminho_tareffa)
|
|
honorarios = le_honorarios(caminho_honorarios)
|
|
|
|
derivacao = deriva_tipos_por_empresa(linhas)
|
|
avisos = list(derivacao.avisos)
|
|
|
|
gerente_por_responsavel: dict[str, str] = {}
|
|
for linha in linhas:
|
|
if linha.responsavel and linha.responsavel not in gerente_por_responsavel:
|
|
gerente_por_responsavel[linha.responsavel] = linha.gerente
|
|
|
|
papeis_por_responsavel: dict[str, list[PapelPorEmpresa]] = {}
|
|
for papel in derivacao.papeis:
|
|
papeis_por_responsavel.setdefault(papel.responsavel, []).append(papel)
|
|
|
|
colaboradores: list[ColaboradorCalculado] = []
|
|
for responsavel, papeis in sorted(papeis_por_responsavel.items()):
|
|
gerente = gerente_por_responsavel.get(responsavel, "")
|
|
departamento_id = gerente_departamento.get(gerente)
|
|
if departamento_id is None:
|
|
avisos.append(
|
|
f'Gerente "{gerente or "(em branco)"}" (colaborador "{responsavel}") não está associado a '
|
|
"nenhum departamento — cadastre essa relação em Configurações → Departamentos; até lá, este "
|
|
"colaborador fica sem critérios nem percentuais calculados."
|
|
)
|
|
colaborador = ColaboradorCalculado(nome=responsavel, gerente=gerente, departamento_id=departamento_id)
|
|
|
|
for papel in papeis:
|
|
honorario = honorarios.get(papel.codigo_empresa)
|
|
colaborador.empresas.append(
|
|
EmpresaCalculada(
|
|
codigo_empresa=papel.codigo_empresa,
|
|
nome_empresa=papel.nome_empresa,
|
|
honorario=honorario if honorario is not None else Decimal("0"),
|
|
honorario_nao_encontrado=honorario is None,
|
|
tipo=papel.tipo,
|
|
)
|
|
)
|
|
|
|
criterios_automaticos = criterios_automaticos_por_departamento.get(departamento_id, [])
|
|
for criterio in criterios_automaticos:
|
|
servicos = SERVICOS_POR_CALCULO_AUTOMATICO.get(criterio.tipo_calculo)
|
|
if not servicos:
|
|
continue
|
|
resultado_entrega = calcula_percentual_entrega(linhas, responsavel, servicos)
|
|
if resultado_entrega.percentual is None:
|
|
# Responsável nunca prestou esse tipo de serviço na competência —
|
|
# o critério não se aplica a ele, não é uma falha (isso é o que
|
|
# permite deixar `papel_aplicavel` em branco nos 3 critérios
|
|
# automáticos: quem não faz aquele serviço fica de fora do
|
|
# cálculo por conta própria, sem precisar restringir por papel).
|
|
valor = "NAO_SE_APLICA"
|
|
elif resultado_entrega.percentual >= criterio.limiar_percentual:
|
|
valor = "SIM"
|
|
else:
|
|
valor = "NAO"
|
|
colaborador.respostas_automaticas.append(
|
|
RespostaAutomatica(
|
|
criterio_id=criterio.criterio_id,
|
|
valor=valor,
|
|
percentual_calculado=resultado_entrega.percentual,
|
|
)
|
|
)
|
|
|
|
colaboradores.append(colaborador)
|
|
|
|
return ResultadoApuracao(colaboradores=colaboradores, avisos=avisos)
|