# /SGMP_PROD/solicitacoes/services.py import logging from collections import Counter from datetime import date, datetime from typing import Any, Dict, Optional, 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 def _status_e_envio_inicial(solicitante: UsuarioSistema) -> Tuple[str, Optional[datetime]]: """ Diretoria cria já como ENVIADA (pula rascunho e aprovação do Head). Demais perfis iniciam em RASCUNHO. """ if solicitante.tem_perfil(UsuarioSistema.Perfil.DIRETORIA): return StatusSolicitacao.ENVIADA, timezone.now() return StatusSolicitacao.RASCUNHO, None # --- 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) ) status_inicial, enviada_em = _status_e_envio_inicial(solicitante) create_kwargs = { "tipo": TipoSolicitacao.DESLIGAMENTO, "solicitante": solicitante, "funcionario": funcionario, "status": status_inicial, } if enviada_em is not None: create_kwargs["enviada_em"] = enviada_em solicitacao = Solicitacao.objects.create(**create_kwargs) 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. """ status_inicial, enviada_em = _status_e_envio_inicial(solicitante) create_kwargs = { "tipo": TipoSolicitacao.ADMISSAO_AUMENTO, "solicitante": solicitante, "funcionario": None, "status": status_inicial, } if enviada_em is not None: create_kwargs["enviada_em"] = enviada_em solicitacao = Solicitacao.objects.create(**create_kwargs) 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.") status_inicial, enviada_em = _status_e_envio_inicial(solicitante) create_kwargs = { "tipo": TipoSolicitacao.ADMISSAO_SUBSTITUICAO, "solicitante": solicitante, "funcionario": funcionario_substituido, "status": status_inicial, } if enviada_em is not None: create_kwargs["enviada_em"] = enviada_em solicitacao = Solicitacao.objects.create(**create_kwargs) 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.") status_inicial, enviada_em = _status_e_envio_inicial(solicitante) create_kwargs = { "tipo": TipoSolicitacao.MOVIMENTACAO, "solicitante": solicitante, "funcionario": funcionario, "status": status_inicial, } if enviada_em is not None: create_kwargs["enviada_em"] = enviada_em solicitacao = Solicitacao.objects.create(**create_kwargs) 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.") # Já disparada (ex.: Diretoria cria direto como ENVIADA; cliente pode chamar enviar de novo) if solicitacao.status == StatusSolicitacao.ENVIADA: return solicitacao 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.") etapas_aptas = [] if usuario.tem_perfil(UsuarioSistema.Perfil.GG): etapas_aptas.append(EtapaAprovacao.GG) if usuario.tem_perfil(UsuarioSistema.Perfil.CONTROLADORIA): etapas_aptas.append(EtapaAprovacao.CONTROLADORIA) if usuario.tem_perfil(UsuarioSistema.Perfil.ADMIN): # ADMIN atua como super-perfil: assume a primeira etapa pendente. if not Parecer.objects.filter(solicitacao=solicitacao, etapa=EtapaAprovacao.GG).exists(): etapa = EtapaAprovacao.GG elif not Parecer.objects.filter(solicitacao=solicitacao, etapa=EtapaAprovacao.CONTROLADORIA).exists(): etapa = EtapaAprovacao.CONTROLADORIA else: raise ValidacaoError("Esta solicitação já possui todos os pareceres técnicos.") else: etapa = next( (e for e in etapas_aptas if not Parecer.objects.filter(solicitacao=solicitacao, etapa=e).exists()), None, ) if not etapa: raise PermissaoError("Apenas GG, Controladoria ou Admin 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 not ( aprovador.tem_perfil(UsuarioSistema.Perfil.DIRETORIA) or aprovador.tem_perfil(UsuarioSistema.Perfil.ADMIN) ): raise PermissaoError("Apenas a Diretoria ou Admin 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 not ( aprovador.tem_perfil(UsuarioSistema.Perfil.HEAD) or aprovador.tem_perfil(UsuarioSistema.Perfil.ADMIN) ): raise PermissaoError("Apenas o Head ou Admin 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 def _status_apos_remover_pareceres(solicitacao: Solicitacao) -> str: """Define status após remoção de um parecer (GG/Ctrl): volta para ENVIADA até ambos existirem de novo.""" tem_gg = Parecer.objects.filter( solicitacao=solicitacao, etapa=EtapaAprovacao.GG ).exists() tem_ctrl = Parecer.objects.filter( solicitacao=solicitacao, etapa=EtapaAprovacao.CONTROLADORIA ).exists() if tem_gg and tem_ctrl: return StatusSolicitacao.AGUARDANDO_DIRETORIA return StatusSolicitacao.ENVIADA @transaction.atomic def reverter_ultima_acao(solicitacao: Solicitacao, dry_run: bool = False) -> dict: """ Desfaz um passo do fluxo, da situação atual para trás (última ação gravada). Ordem de reversão: 1. FINALIZADA com Aprovacao DIRETORIA → remove decisão da Diretoria. 2. AGUARDANDO_DIRETORIA → remove o parecer mais recente (GG ou Controladoria). 3. ENVIADA → remove o parecer mais recente, se houver; senão remove Aprovacao HEAD (volta AGUARDANDO_HEAD). 4. AGUARDANDO_HEAD (sem aprovação Head) → volta para RASCUNHO (desfaz envio ao Head). Não trata REPROVADA nem estados legados (APROVADA_*); use o Admin ou script dedicado. Retorna dict com chaves: acao (str), detalhe (str). Se dry_run=True, não grava alterações. """ pk = solicitacao.pk s = Solicitacao.objects.select_for_update().get(pk=pk) if not dry_run else Solicitacao.objects.get(pk=pk) st = s.status acao = "" detalhe = "" # 1) Decisão da Diretoria if st == StatusSolicitacao.FINALIZADA: qs_dir = Aprovacao.objects.filter(solicitacao=s, etapa=EtapaAprovacao.DIRETORIA) if not qs_dir.exists(): raise ValidacaoError( "FINALIZADA sem registro de Aprovacao na DIRETORIA — estado inconsistente; corrija manualmente." ) novo = _status_apos_remover_pareceres(s) acao = "remover_aprovacao_diretoria" detalhe = f"Remover decisão DIRETORIA; status → {novo}" if not dry_run: qs_dir.delete() s.finalizada_em = None s.status = novo s.save() logger.warning( "reverter_ultima_acao: removida Aprovacao DIRETORIA da solicitação %s → %s", s.id, novo, ) return {"acao": acao, "detalhe": detalhe, "novo_status": novo} # 2) Estava aguardando Diretoria: desfaz último parecer técnico if st == StatusSolicitacao.AGUARDANDO_DIRETORIA: ultimo = ( Parecer.objects.filter(solicitacao=s) .order_by("-criado_em", "-id") .first() ) if not ultimo: raise ValidacaoError( "AGUARDANDO_DIRETORIA sem pareceres — inconsistente; corrija manualmente." ) et = ultimo.etapa acao = "remover_parecer" detalhe = f"Remover parecer {et} (mais recente); status → ENVIADA" novo = StatusSolicitacao.ENVIADA if not dry_run: ultimo.delete() s.status = novo s.save() logger.warning( "reverter_ultima_acao: removido Parecer %s da solicitação %s → ENVIADA", et, s.id, ) return {"acao": acao, "detalhe": detalhe, "novo_status": novo} # 3) ENVIADA: parecer mais recente OU aprovação Head if st == StatusSolicitacao.ENVIADA: ultimo_p = ( Parecer.objects.filter(solicitacao=s) .order_by("-criado_em", "-id") .first() ) if ultimo_p: et = ultimo_p.etapa acao = "remover_parecer" if dry_run: c = Counter( Parecer.objects.filter(solicitacao=s).values_list("etapa", flat=True) ) c[et] -= 1 tem_gg = c[EtapaAprovacao.GG] > 0 tem_ctrl = c[EtapaAprovacao.CONTROLADORIA] > 0 novo = ( StatusSolicitacao.AGUARDANDO_DIRETORIA if (tem_gg and tem_ctrl) else StatusSolicitacao.ENVIADA ) detalhe = f"Remover parecer {et} (mais recente); status → {novo}" return {"acao": acao, "detalhe": detalhe, "novo_status": novo} ultimo_p.delete() s.refresh_from_db() s.status = _status_apos_remover_pareceres(s) s.save() logger.warning( "reverter_ultima_acao: removido Parecer %s da solicitação %s → %s", et, s.id, s.status, ) return { "acao": acao, "detalhe": f"Remover parecer {et}; status → {s.status}", "novo_status": s.status, } ap_head = Aprovacao.objects.filter(solicitacao=s, etapa=EtapaAprovacao.HEAD).first() if ap_head: acao = "remover_aprovacao_head" detalhe = "Remover aprovação HEAD; status → AGUARDANDO_HEAD; limpar enviada_em" novo = StatusSolicitacao.AGUARDANDO_HEAD if not dry_run: ap_head.delete() s.status = novo s.enviada_em = None s.save() logger.warning( "reverter_ultima_acao: removida Aprovacao HEAD da solicitação %s → AGUARDANDO_HEAD", s.id, ) return {"acao": acao, "detalhe": detalhe, "novo_status": novo} raise ValidacaoError( "ENVIADA sem pareceres e sem Aprovacao HEAD — nada a reverter (ex.: criada pela Diretoria já neste status)." ) # 4) Aguardando Head: desfaz o envio (volta rascunho) if st == StatusSolicitacao.AGUARDANDO_HEAD: if Aprovacao.objects.filter(solicitacao=s, etapa=EtapaAprovacao.HEAD).exists(): raise ValidacaoError( "AGUARDANDO_HEAD com Aprovacao HEAD já registrada — estado inconsistente; use reversão após corrigir status." ) acao = "desfazer_envio_head" detalhe = "Voltar de AGUARDANDO_HEAD para RASCUNHO (desfaz envio ao Head)" novo = StatusSolicitacao.RASCUNHO if not dry_run: s.status = novo s.save() logger.warning( "reverter_ultima_acao: solicitação %s voltou de AGUARDANDO_HEAD → RASCUNHO", s.id, ) return {"acao": acao, "detalhe": detalhe, "novo_status": novo} raise ValidacaoError( f"Reversão não suportada para o status atual ({st}). " "Estados como REPROVADA ou APROVADA_* legados exigem correção manual." )