# /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