495 lines
17 KiB
Python
495 lines
17 KiB
Python
# /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 já 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; só 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
|
|
|
|
|