sgmp/solicitacoes/services.py

495 lines
17 KiB
Python
Raw Normal View History

2026-03-09 18:46:01 +00:00
# /SGMP_PROD/solicitacoes/services.py
import logging
from datetime import date
from typing import Dict, Any, Tuple
from django.db import transaction
from django.utils import timezone
from .models import (
PessoaRM,
Solicitacao,
Desligamento,
AdmissaoSubstituicao,
AdmissaoAumentoQuadro,
Movimentacao,
Aprovacao,
Parecer,
UsuarioSistema,
TipoSolicitacao,
StatusSolicitacao,
DecisaoAprovacao,
EtapaAprovacao,
)
from .intf_sqlserver import listar_para_selecionar_colaborador, verificar_estabilidades_colaborador
from .intf_winthor import buscar_colaborador_oracle
# Configuração do Logger para rastreabilidade
logger = logging.getLogger(__name__)
STATUS_POR_ETAPA = {
EtapaAprovacao.HEAD: StatusSolicitacao.ENVIADA,
EtapaAprovacao.DIRETORIA: StatusSolicitacao.FINALIZADA,
}
# --- Exceções de Domínio ---
# Usar exceções customizadas torna o código mais explícito e fácil de
# tratar nas camadas superiores (views, APIs).
class SolicitacaoError(Exception):
"""Classe base para erros relacionados a solicitações."""
pass
class ValidacaoError(SolicitacaoError):
"""Lançada quando uma regra de negócio é violada."""
pass
class PermissaoError(SolicitacaoError):
"""Lançada quando um usuário tenta executar uma ação não permitida."""
pass
def sincronizar_colaboradores_rm() -> Tuple[int, int]:
"""
Executa a sincronização de colaboradores do TOTVS RM para o SGMP.
Esta função é responsável por:
1. Buscar todos os colaboradores ativos no RM.
2. Usar 'update_or_create' para inserir novos ou atualizar existentes.
3. Tentar, de forma resiliente, enriquecer o dado com a matrícula do Winthor.
A lógica de integração fica isolada aqui, protegendo o resto do sistema
de sua complexidade.
Retorna:
Uma tupla (criados, atualizados) com a contagem de registros.
"""
logger.info("Iniciando sincronização de colaboradores com o TOTVS RM.")
dados_rm = listar_para_selecionar_colaborador()
criados = 0
atualizados = 0
agora = timezone.now()
for row in dados_rm:
id_rm = f"{row['CODCOLIGADA']}-{row['CHAPA']}"
cpf = row.get('CPF')
matricula_winthor = None
# --- Tratamento de Integração Satélite (Winthor) ---
# A busca no Winthor é "best-effort". Uma falha aqui não deve
# impedir a sincronização principal com o RM.
if cpf:
try:
dados_winthor = buscar_colaborador_oracle(cpf)
if dados_winthor:
matricula_winthor = dados_winthor.get('matricula')
except Exception as e:
logger.error(f"Falha ao buscar CPF {cpf} no Winthor: {e}")
# O 'update_or_create' é atômico por padrão e a forma mais segura
# e eficiente de realizar a sincronização.
_, created = PessoaRM.objects.update_or_create(
id_rm=id_rm,
defaults={
"matricula": row["CHAPA"],
"nome": row["NOME"],
"cpf": cpf,
"cargo": row["FUNCAO"],
"setor": row["SECAO"],
"centro_custo": row["CODSECAO"],
"data_admissao": row["DATAADMISSAO"],
"situacao": row["CODSITUACAO"],
"cod_funcao": row["CODFUNCAO"],
"salario": row["SALARIO"],
"cod_sindicato": row["CODSINDICATO"],
"saldo_banco_horas_minutos": row.get("SALDO_MINUTOS"),
"inicio_periodo_banco_horas": row.get("INICIOPER"),
"fim_periodo_banco_horas": row.get("FIMPER"),
"matricula_winthor": matricula_winthor,
"sincronizado_em": agora,
},
)
if created:
criados += 1
else:
atualizados += 1
logger.info(f"Sincronização concluída. Criados: {criados}, Atualizados: {atualizados}.")
return criados, atualizados
# --- Service de Gestão de Solicitações (Core do Domínio) ---
@transaction.atomic
def criar_solicitacao_desligamento(
solicitante: UsuarioSistema,
funcionario: PessoaRM,
tipo_desligamento: str,
aviso_previo: str,
motivo: str,
data_prevista_desligamento: date,
observacoes: str = "",
arquivo_pedido = None
) -> Solicitacao:
"""
Cria uma solicitação de Desligamento de forma transacional.
Validações:
- Garante que não existe outra solicitação em aberto para o mesmo funcionário.
- Verifica estabilidades legais/operacionais que podem bloquear o desligamento.
O decorador @transaction.atomic garante que a criação da Solicitação
e do Desligamento ocorram juntas. Se uma falhar, a outra é revertida.
"""
if Solicitacao.objects.filter(
funcionario=funcionario,
status__in=[
StatusSolicitacao.RASCUNHO,
StatusSolicitacao.ENVIADA,
StatusSolicitacao.APROVADA_GG,
StatusSolicitacao.APROVADA_CONTROLADORIA,
StatusSolicitacao.APROVADA_DIRETORIA
]
).exists():
raise ValidacaoError("Já existe uma solicitação em andamento para este funcionário.")
# Verifica estabilidades que bloqueiam o desligamento
estabilidades = verificar_estabilidades_colaborador(funcionario.id_rm)
estabilidades_bloqueantes = [e for e in estabilidades if e.get('bloqueado', False)]
if estabilidades_bloqueantes:
mensagens = [e['mensagem'] for e in estabilidades_bloqueantes]
raise ValidacaoError(
"Desligamento bloqueado por estabilidade legal/operacional:\n" +
"\n".join(f"{msg}" for msg in mensagens)
)
solicitacao = Solicitacao.objects.create(
tipo=TipoSolicitacao.DESLIGAMENTO,
solicitante=solicitante,
funcionario=funcionario,
status=StatusSolicitacao.RASCUNHO
)
Desligamento.objects.create(
solicitacao=solicitacao,
tipo_desligamento=tipo_desligamento,
aviso_previo=aviso_previo,
motivo=motivo,
data_prevista_desligamento=data_prevista_desligamento,
observacoes=observacoes,
arquivo_pedido=arquivo_pedido
)
logger.info(f"Solicitação de Desligamento {solicitacao.id} criada para {funcionario.nome} por {solicitante.nome}.")
return solicitacao
@transaction.atomic
def criar_solicitacao_aumento_quadro(
solicitante: UsuarioSistema,
dados_admissao: Dict[str, Any]
) -> Solicitacao:
"""
Cria uma solicitação de Admissão por Aumento de Quadro.
Este tipo de solicitação não possui um 'funcionario' vinculado
inicialmente, pois representa a criação de uma nova vaga.
"""
solicitacao = Solicitacao.objects.create(
tipo=TipoSolicitacao.ADMISSAO_AUMENTO,
solicitante=solicitante,
# 'funcionario' é None por definição aqui
funcionario=None,
status=StatusSolicitacao.RASCUNHO
)
AdmissaoAumentoQuadro.objects.create(
solicitacao=solicitacao,
**dados_admissao
)
logger.info(f"Solicitação de Aumento de Quadro {solicitacao.id} criada por {solicitante.nome}.")
return solicitacao
@transaction.atomic
def criar_solicitacao_substituicao(
solicitante: UsuarioSistema,
funcionario_substituido: PessoaRM,
dados_admissao: Dict[str, Any],
) -> Solicitacao:
"""
Cria uma solicitação de Admissão por Substituição.
Regras:
- Deve existir um funcionário sendo substituído
- Não pode haver outra solicitação ativa para esse funcionário
- Permite funcionários desligados, pois a substituição é exatamente para substituir quem saiu
"""
# Não valida situação desligada para substituição, pois é esperado que o funcionário
# substituído possa estar desligado (essa é a razão da substituição)
if Solicitacao.objects.filter(
funcionario=funcionario_substituido,
status__in=[
StatusSolicitacao.RASCUNHO,
StatusSolicitacao.ENVIADA,
StatusSolicitacao.APROVADA_GG,
StatusSolicitacao.APROVADA_CONTROLADORIA,
StatusSolicitacao.APROVADA_DIRETORIA,
],
).exists():
raise ValidacaoError("Já existe uma solicitação em andamento para este funcionário.")
solicitacao = Solicitacao.objects.create(
tipo=TipoSolicitacao.ADMISSAO_SUBSTITUICAO,
solicitante=solicitante,
funcionario=funcionario_substituido,
status=StatusSolicitacao.RASCUNHO,
)
AdmissaoSubstituicao.objects.create(
solicitacao=solicitacao,
**dados_admissao,
)
logger.info(
f"Solicitação de Substituição {solicitacao.id} criada para "
f"{funcionario_substituido.nome} por {solicitante.nome}."
)
return solicitacao
@transaction.atomic
def criar_solicitacao_movimentacao(
solicitante: UsuarioSistema,
funcionario: PessoaRM,
dados_movimentacao: Dict[str, Any],
) -> Solicitacao:
"""
Cria uma solicitação de Movimentação Interna.
Regras:
- Funcionário deve estar ativo
- Não pode haver outra solicitação ativa para o mesmo funcionário
"""
if funcionario.situacao == 'D':
raise ValidacaoError("Não é possível movimentar um funcionário desligado.")
if Solicitacao.objects.filter(
funcionario=funcionario,
status__in=[
StatusSolicitacao.RASCUNHO,
StatusSolicitacao.ENVIADA,
StatusSolicitacao.APROVADA_GG,
StatusSolicitacao.APROVADA_CONTROLADORIA,
StatusSolicitacao.APROVADA_DIRETORIA,
],
).exists():
raise ValidacaoError("Já existe uma solicitação em andamento para este funcionário.")
solicitacao = Solicitacao.objects.create(
tipo=TipoSolicitacao.MOVIMENTACAO,
solicitante=solicitante,
funcionario=funcionario,
status=StatusSolicitacao.RASCUNHO,
)
Movimentacao.objects.create(
solicitacao=solicitacao,
**dados_movimentacao,
)
logger.info(
f"Solicitação de Movimentação {solicitacao.id} criada para "
f"{funcionario.nome} por {solicitante.nome}."
)
return solicitacao
@transaction.atomic
def enviar_solicitacao(solicitacao: Solicitacao, usuario: UsuarioSistema) -> Solicitacao:
"""
Muda o status de uma solicitação de 'Rascunho' para 'Aguardando Head'.
O Head deve aprovar ou reprovar; após aprovação do Head a solicitação
passa para ENVIADA (e enviada_em é preenchido). Reprovação do Head REPROVADA.
Validações:
- Apenas o solicitante pode enviar.
- A solicitação deve estar no estado 'Rascunho'.
"""
if solicitacao.solicitante != usuario:
raise PermissaoError("Apenas o solicitante pode enviar a solicitação.")
if not solicitacao.pode_enviar():
raise ValidacaoError(f"A solicitação não pode ser enviada no status atual ({solicitacao.get_status_display()}).")
solicitacao.status = StatusSolicitacao.AGUARDANDO_HEAD
solicitacao.save()
# enviada_em será preenchido quando o Head aprovar (em aprovar_reprovar_por_head)
logger.info(f"Solicitação {solicitacao.id} enviada para aprovação do Head por {usuario.nome}.")
return solicitacao
@transaction.atomic
def registrar_parecer(
solicitacao: Solicitacao,
usuario: UsuarioSistema,
texto: str,
anexo=None
) -> Parecer:
"""
Registra um parecer de GG ou CONTROLADORIA sobre uma solicitação.
Não altera o status da solicitação diretamente.
Quando ambos os pareceres (GG e CONTROLADORIA) são dados,
a solicitação muda automaticamente para AGUARDANDO_DIRETORIA.
Validações:
- Verifica se o usuário pode dar parecer (GG ou CONTROLADORIA)
- Verifica se a solicitação está no status ENVIADA
- Impede pareceres duplicados para a mesma etapa
"""
if not solicitacao.pode_dar_parecer(usuario):
raise PermissaoError("Usuário não pode dar parecer nesta solicitação.")
# Mapeia perfil para etapa
mapa_perfil_etapa = {
UsuarioSistema.Perfil.GG: EtapaAprovacao.GG,
UsuarioSistema.Perfil.CONTROLADORIA: EtapaAprovacao.CONTROLADORIA,
}
etapa = mapa_perfil_etapa.get(usuario.perfil)
if not etapa:
raise PermissaoError("Apenas GG e Controladoria podem dar parecer.")
# Verifica se já existe parecer para esta etapa
if Parecer.objects.filter(solicitacao=solicitacao, etapa=etapa).exists():
raise ValidacaoError(f"Já existe um parecer da {etapa.label} para esta solicitação.")
# Cria o parecer
parecer = Parecer.objects.create(
solicitacao=solicitacao,
etapa=etapa,
usuario=usuario,
texto=texto,
anexo=anexo
)
# Verifica se ambos os pareceres foram dados
parecer_gg = Parecer.objects.filter(solicitacao=solicitacao, etapa=EtapaAprovacao.GG).exists()
parecer_controladoria = Parecer.objects.filter(solicitacao=solicitacao, etapa=EtapaAprovacao.CONTROLADORIA).exists()
if parecer_gg and parecer_controladoria:
# Ambos os pareceres foram dados, muda status para AGUARDANDO_DIRETORIA
solicitacao.status = StatusSolicitacao.AGUARDANDO_DIRETORIA
solicitacao.save()
logger.info(f"Solicitação {solicitacao.id} mudou para AGUARDANDO_DIRETORIA após ambos os pareceres serem dados.")
logger.info(f"Parecer da {etapa.label} registrado para a solicitação {solicitacao.id} por {usuario.nome}.")
return parecer
@transaction.atomic
def aprovar_reprovar_solicitacao(
solicitacao: Solicitacao,
aprovador: UsuarioSistema,
decisao: str,
justificativa: str = ""
) -> Solicitacao:
"""
Registra uma decisão (Aprovação/Reprovação) da DIRETORIA.
Apenas a DIRETORIA pode aprovar/reprovar solicitações.
A solicitação deve estar no status AGUARDANDO_DIRETORIA.
Validações:
- Verifica se o aprovador é da DIRETORIA
- Verifica se a solicitação está aguardando aprovação da Diretoria
- Justificativa é obrigatória apenas para reprovação
"""
if aprovador.perfil != UsuarioSistema.Perfil.DIRETORIA:
raise PermissaoError("Apenas a Diretoria pode aprovar/reprovar solicitações.")
if solicitacao.status != StatusSolicitacao.AGUARDANDO_DIRETORIA:
raise ValidacaoError("A solicitação não está aguardando aprovação da Diretoria.")
etapa_atual = solicitacao.etapa_atual()
if etapa_atual != EtapaAprovacao.DIRETORIA:
raise ValidacaoError("A solicitação não está em uma etapa de aprovação válida.")
# Valida justificativa apenas para reprovação
if decisao == DecisaoAprovacao.REPROVADO and not justificativa.strip():
raise ValidacaoError("Justificativa é obrigatória para reprovação.")
# Cria o registro de decisão
Aprovacao.objects.create(
solicitacao=solicitacao,
etapa=etapa_atual,
decisao=decisao,
usuario=aprovador,
justificativa=justificativa or "Aprovado" # Valor padrão para aprovação
)
if decisao == DecisaoAprovacao.REPROVADO:
solicitacao.status = StatusSolicitacao.REPROVADA
solicitacao.finalizada_em = timezone.now()
else:
solicitacao.status = STATUS_POR_ETAPA[etapa_atual]
if solicitacao.status == StatusSolicitacao.FINALIZADA:
solicitacao.finalizada_em = timezone.now()
solicitacao.save()
logger.info(f"Decisão '{decisao}' registrada pela Diretoria para a solicitação {solicitacao.id} por {aprovador.nome}.")
return solicitacao
@transaction.atomic
def aprovar_reprovar_por_head(
solicitacao: Solicitacao,
aprovador: UsuarioSistema,
decisao: str,
justificativa: str = "",
) -> Solicitacao:
"""
Registra a decisão (Aprovação/Reprovação) do HEAD sobre o rascunho enviado pelo gestor.
Apenas o perfil HEAD pode usar esta função.
A solicitação deve estar no status AGUARDANDO_HEAD.
Se aprovado: status ENVIADA e enviada_em é preenchido.
Se reprovado: status REPROVADA e finalizada_em é preenchido.
"""
if aprovador.perfil != UsuarioSistema.Perfil.HEAD:
raise PermissaoError("Apenas o Head pode aprovar/reprovar solicitações nesta etapa.")
if solicitacao.status != StatusSolicitacao.AGUARDANDO_HEAD:
raise ValidacaoError("A solicitação não está aguardando aprovação do Head.")
etapa_atual = solicitacao.etapa_atual()
if etapa_atual != EtapaAprovacao.HEAD:
raise ValidacaoError("A solicitação não está em etapa de aprovação do Head.")
if decisao == DecisaoAprovacao.REPROVADO and not justificativa.strip():
raise ValidacaoError("Justificativa é obrigatória para reprovação.")
Aprovacao.objects.create(
solicitacao=solicitacao,
etapa=EtapaAprovacao.HEAD,
decisao=decisao,
usuario=aprovador,
justificativa=justificativa or "Aprovado",
)
if decisao == DecisaoAprovacao.REPROVADO:
solicitacao.status = StatusSolicitacao.REPROVADA
solicitacao.finalizada_em = timezone.now()
else:
solicitacao.status = StatusSolicitacao.ENVIADA
solicitacao.enviada_em = timezone.now()
solicitacao.save()
logger.info(
f"Decisão '{decisao}' registrada pelo Head para a solicitação {solicitacao.id} por {aprovador.nome}."
)
return solicitacao