762 lines
25 KiB
Python
762 lines
25 KiB
Python
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
import uuid
|
|
|
|
class BaseModel(models.Model):
|
|
"""
|
|
Modelo base abstrato utilizado por todas as entidades do SGMP.
|
|
|
|
Centraliza atributos comuns de persistência, garantindo:
|
|
- Identificação única global via UUID (facilita integrações externas)
|
|
- Rastreabilidade temporal (criação e última atualização)
|
|
|
|
O uso de UUID como chave primária evita dependência de IDs sequenciais
|
|
e facilita sincronização entre sistemas distintos (SGMP, RM, Winthor).
|
|
"""
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
criado_em = models.DateTimeField(auto_now_add=True)
|
|
atualizado_em = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
class TipoSolicitacao(models.TextChoices):
|
|
"""
|
|
Enumeração que define os tipos de solicitações suportadas pelo SGMP.
|
|
|
|
Cada tipo representa um processo de RH distinto, podendo possuir:
|
|
- regras de negócio específicas
|
|
- dados complementares próprios
|
|
- fluxos de validação e integração diferentes
|
|
|
|
Exemplos:
|
|
- Desligamento
|
|
- Admissão (substituição ou aumento de quadro)
|
|
- Movimentação interna
|
|
"""
|
|
DESLIGAMENTO = "DESLIGAMENTO", _("Desligamento")
|
|
ADMISSAO_SUBSTITUICAO = "ADM_SUBSTITUICAO", _("Admissão por Substituição")
|
|
ADMISSAO_AUMENTO = "ADM_AUMENTO", _("Admissão por Aumento de Quadro")
|
|
MOVIMENTACAO = "MOVIMENTACAO", _("Movimentação")
|
|
|
|
class StatusSolicitacao(models.TextChoices):
|
|
"""
|
|
Enumeração que representa o estado atual de uma solicitação
|
|
dentro do fluxo de aprovação do SGMP.
|
|
|
|
O status reflete a posição da solicitação no processo,
|
|
controlando permissões de edição, envio, aprovação e finalização.
|
|
|
|
O fluxo é progressivo e controlado, permitindo auditoria
|
|
e rastreabilidade do ciclo de vida da solicitação.
|
|
"""
|
|
RASCUNHO = "RASCUNHO", _("Rascunho")
|
|
AGUARDANDO_HEAD = "AGUARDANDO_HEAD", _("Aguardando Head")
|
|
ENVIADA = "ENVIADA", _("Enviada")
|
|
APROVADA_GG = "APROVADA_GG", _("Aprovada GG")
|
|
APROVADA_CONTROLADORIA = "APROVADA_CONTROLADORIA", _("Aprovada Controladoria")
|
|
APROVADA_DIRETORIA = "APROVADA_DIRETORIA", _("Aprovada Diretoria")
|
|
AGUARDANDO_DIRETORIA = "AGUARDANDO_DIRETORIA", _("Aguardando Diretoria")
|
|
FINALIZADA = "FINALIZADA", _("Finalizada")
|
|
REPROVADA = "REPROVADA", _("Reprovada")
|
|
|
|
class EtapaAprovacao(models.TextChoices):
|
|
"""
|
|
Enumeração que define as etapas formais de aprovação
|
|
dentro do fluxo decisório do SGMP.
|
|
|
|
Cada etapa corresponde a um papel organizacional específico,
|
|
responsável por validar a solicitação sob sua ótica
|
|
(pessoas, orçamento, estratégia).
|
|
"""
|
|
HEAD = "HEAD", _("Head")
|
|
GG = "GG", _("Gente e Gestão")
|
|
CONTROLADORIA = "CONTROLADORIA", _("Controladoria")
|
|
DIRETORIA = "DIRETORIA", _("Diretoria")
|
|
|
|
class DecisaoAprovacao(models.TextChoices):
|
|
"""
|
|
Enumeração que representa a decisão tomada em uma etapa
|
|
de aprovação de uma solicitação.
|
|
|
|
A decisão é registrada de forma explícita para garantir:
|
|
- clareza
|
|
- rastreabilidade
|
|
- auditoria do processo decisório
|
|
"""
|
|
APROVADO = "APROVADO", _("Aprovado")
|
|
REPROVADO = "REPROVADO", _("Reprovado")
|
|
|
|
class UsuarioSistema(BaseModel):
|
|
"""
|
|
Representa um usuário que possui acesso ao SGMP.
|
|
|
|
Este modelo não representa necessariamente um funcionário ativo,
|
|
mas sim um agente que interage com o sistema, como:
|
|
- gestores
|
|
- equipe de Gente & Gestão
|
|
- controladoria
|
|
- diretoria
|
|
|
|
O perfil do usuário define seu papel no fluxo de solicitações
|
|
e suas permissões de visualização, edição e aprovação.
|
|
"""
|
|
matricula = models.CharField(max_length=20, unique=True)
|
|
nome = models.CharField(max_length=255)
|
|
ativo = models.BooleanField(default=True)
|
|
|
|
class Perfil(models.TextChoices):
|
|
GESTOR = "GESTOR", _("Gestor")
|
|
HEAD = "HEAD", _("Head")
|
|
GG = "GG", _("Gente e Gestão")
|
|
CONTROLADORIA = "CONTROLADORIA", _("Controladoria")
|
|
DIRETORIA = "DIRETORIA", _("Diretoria")
|
|
|
|
perfil = models.CharField(
|
|
max_length=20,
|
|
choices=Perfil.choices,
|
|
default=Perfil.GESTOR
|
|
)
|
|
|
|
def tem_perfil(self, perfil: str) -> bool:
|
|
"""
|
|
Verifica se o usuário possui o perfil informado, considerando
|
|
o perfil principal e perfis extras.
|
|
"""
|
|
if self.perfil == perfil:
|
|
return True
|
|
# Evita import cíclico: FK de UsuarioPerfilExtra usa related_name="perfis_extras"
|
|
return self.perfis_extras.filter(perfil=perfil).exists()
|
|
|
|
def perfis_ativos(self):
|
|
"""
|
|
Retorna lista de códigos de perfis ativos para o usuário,
|
|
começando pelo perfil principal e incluindo extras (sem repetir).
|
|
"""
|
|
extras = list(self.perfis_extras.values_list("perfil", flat=True))
|
|
# Garante que o principal venha primeiro e não seja duplicado
|
|
return [self.perfil] + [p for p in extras if p != self.perfil]
|
|
|
|
def __str__(self):
|
|
return f"{self.nome} ({self.matricula})"
|
|
|
|
|
|
class UsuarioPerfilExtra(BaseModel):
|
|
"""
|
|
Representa perfis adicionais atribuídos a um usuário do sistema.
|
|
|
|
Exemplo: perfil principal GESTOR + perfil extra HEAD para permitir
|
|
atuar como Head sem perder o comportamento padrão de gestor.
|
|
"""
|
|
usuario = models.ForeignKey(
|
|
UsuarioSistema,
|
|
on_delete=models.CASCADE,
|
|
related_name="perfis_extras",
|
|
)
|
|
perfil = models.CharField(
|
|
max_length=20,
|
|
choices=UsuarioSistema.Perfil.choices,
|
|
)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["usuario", "perfil"],
|
|
name="unique_usuario_perfil_extra",
|
|
),
|
|
]
|
|
verbose_name = "Perfil extra de usuário"
|
|
verbose_name_plural = "Perfis extras de usuário"
|
|
|
|
def __str__(self):
|
|
return f"{self.usuario} -> {self.perfil}"
|
|
|
|
|
|
class HeadGestor(BaseModel):
|
|
"""
|
|
Vínculo Head → Gestores: define quais gestores um usuário com perfil HEAD
|
|
aprova. Quando o Head está cadastrado, o dashboard mostra apenas
|
|
solicitações AGUARDANDO_HEAD cujo solicitante está nesta lista.
|
|
"""
|
|
head = models.ForeignKey(
|
|
UsuarioSistema,
|
|
on_delete=models.CASCADE,
|
|
related_name="gestores_vinculados",
|
|
)
|
|
gestor = models.ForeignKey(
|
|
UsuarioSistema,
|
|
on_delete=models.CASCADE,
|
|
related_name="heads_que_me_aprovam",
|
|
)
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["head", "gestor"], name="unique_head_gestor"),
|
|
]
|
|
verbose_name = "Head → Gestor"
|
|
verbose_name_plural = "Head → Gestores"
|
|
|
|
def __str__(self):
|
|
return f"{self.head} aprova {self.gestor}"
|
|
|
|
|
|
def matriculas_gestores_do_head(usuario: "UsuarioSistema") -> list:
|
|
"""
|
|
Retorna a lista de matrículas dos gestores que o usuário (HEAD) aprova.
|
|
Se o usuário não for HEAD ou não tiver gestores vinculados, retorna lista vazia.
|
|
"""
|
|
if not usuario or not usuario.tem_perfil(UsuarioSistema.Perfil.HEAD):
|
|
return []
|
|
return list(
|
|
HeadGestor.objects.filter(head=usuario).values_list("gestor__matricula", flat=True)
|
|
)
|
|
|
|
|
|
class PessoaRM(BaseModel):
|
|
"""
|
|
Representa um funcionário sincronizado a partir do TOTVS RM.
|
|
|
|
Este modelo armazena um *snapshot* dos dados cadastrais e organizacionais
|
|
do colaborador, utilizados pelo SGMP para iniciar e acompanhar processos
|
|
internos de RH (ex: desligamento, admissão, movimentação).
|
|
|
|
⚠️ Importante:
|
|
- O TOTVS RM é a origem da verdade.
|
|
- Os dados aqui persistidos representam o estado do colaborador
|
|
no momento da sincronização.
|
|
- Alterações posteriores no RM NÃO atualizam este registro
|
|
automaticamente, exceto via rotina de sincronização.
|
|
|
|
-------------------------
|
|
Query de origem (consolidada)
|
|
-------------------------
|
|
|
|
A sincronização parte da tabela PFUNC (cadastro de pessoas),
|
|
utilizando LEFT JOIN para dados auxiliares, garantindo que
|
|
TODOS os colaboradores ativos sejam retornados, mesmo que
|
|
não possuam banco de horas.
|
|
|
|
WITH SALDO_ATUAL AS (
|
|
SELECT
|
|
SB.CODCOLIGADA,
|
|
SB.CHAPA,
|
|
SB.INICIOPER,
|
|
SB.FIMPER,
|
|
((SB.EXTRAANT - SB.ATRASOANT - SB.FALTAANT) +
|
|
(SB.EXTRAATU - SB.ATRASOATU - SB.FALTAATU)) AS SALDO_MINUTOS,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY SB.CODCOLIGADA, SB.CHAPA
|
|
ORDER BY SB.FIMPER DESC
|
|
) AS RN
|
|
FROM ASALDOBANCOHOR SB
|
|
)
|
|
SELECT
|
|
PF.CODCOLIGADA,
|
|
PF.CHAPA,
|
|
PF.NOME,
|
|
P.CPF,
|
|
PS.DESCRICAO AS SECAO,
|
|
PU.NOME AS FUNCAO,
|
|
PF.DATAADMISSAO,
|
|
PF.CODSITUACAO,
|
|
PF.CODSECAO,
|
|
PF.CODFUNCAO,
|
|
PF.SALARIO,
|
|
PF.CODSINDICATO,
|
|
SA.INICIOPER,
|
|
SA.FIMPER,
|
|
SA.SALDO_MINUTOS
|
|
FROM PFUNC PF
|
|
JOIN PPESSOA P ON P.CODIGO = PF.CODPESSOA
|
|
JOIN PSECAO PS ON PS.CODCOLIGADA = PF.CODCOLIGADA AND PS.CODIGO = PF.CODSECAO
|
|
JOIN PFUNCAO PU ON PU.CODCOLIGADA = PF.CODCOLIGADA AND PU.CODIGO = PF.CODFUNCAO
|
|
LEFT JOIN SALDO_ATUAL SA
|
|
ON SA.CODCOLIGADA = PF.CODCOLIGADA
|
|
AND SA.CHAPA = PF.CHAPA
|
|
AND SA.RN = 1
|
|
WHERE PF.CODSITUACAO <> 'D';
|
|
|
|
-------------------------
|
|
Origem dos dados no RM
|
|
-------------------------
|
|
- PFUNC → dados funcionais e vínculo
|
|
- PPESSOA → dados pessoais básicos
|
|
- PSECAO → estrutura organizacional
|
|
- PFUNCAO → função/cargo
|
|
- ASALDOBANCOHOR → banco de horas (dado auxiliar e opcional)
|
|
|
|
-------------------------
|
|
Observações de domínio
|
|
-------------------------
|
|
- Banco de horas NÃO é obrigatório para todos os colaboradores.
|
|
- Dados de banco de horas são usados apenas para validações
|
|
e apoio a decisões (ex: desligamento).
|
|
- O SGMP nunca utiliza ASALDOBANCOHOR como base de cadastro.
|
|
"""
|
|
|
|
# Identificação no RM
|
|
id_rm = models.CharField(
|
|
max_length=20,
|
|
unique=True,
|
|
help_text="Identificador único do colaborador no RM (CODCOLIGADA + CHAPA)"
|
|
)
|
|
matricula = models.CharField(
|
|
max_length=20,
|
|
unique=True,
|
|
help_text="Matrícula do colaborador no RM (PFUNC.CHAPA)"
|
|
)
|
|
|
|
# Dados cadastrais básicos
|
|
nome = models.CharField(max_length=255)
|
|
cargo = models.CharField(
|
|
max_length=255,
|
|
help_text="Nome da função/cargo atual do colaborador"
|
|
)
|
|
setor = models.CharField(
|
|
max_length=255,
|
|
help_text="Descrição da seção/departamento atual"
|
|
)
|
|
centro_custo = models.CharField(
|
|
max_length=50,
|
|
help_text="Código da seção no RM (PFUNC.CODSECAO)"
|
|
)
|
|
cpf = models.CharField(
|
|
max_length=14,
|
|
null=True,
|
|
blank=True,
|
|
help_text="CPF do colaborador (PPESSOA.CPF)"
|
|
)
|
|
|
|
data_admissao = models.DateField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Data de admissão do colaborador no RM"
|
|
)
|
|
|
|
situacao = models.CharField(
|
|
max_length=5,
|
|
null=True,
|
|
blank=True,
|
|
help_text="Situação do colaborador no RM (PFUNC.CODSITUACAO)"
|
|
)
|
|
|
|
cod_funcao = models.CharField(
|
|
max_length=50,
|
|
null=True,
|
|
blank=True,
|
|
help_text="Código da função/cargo no RM (PFUNC.CODFUNCAO)"
|
|
)
|
|
|
|
salario = models.DecimalField(
|
|
max_digits=12,
|
|
decimal_places=2,
|
|
null=True,
|
|
blank=True,
|
|
help_text="Salário atual do colaborador no RM"
|
|
)
|
|
|
|
cod_sindicato = models.CharField(
|
|
max_length=10,
|
|
null=True,
|
|
blank=True,
|
|
help_text="Código do sindicato no RM"
|
|
)
|
|
|
|
saldo_banco_horas_minutos = models.IntegerField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Saldo atual de banco de horas em minutos (se aplicável)"
|
|
)
|
|
|
|
inicio_periodo_banco_horas = models.DateField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Início do período do último banco de horas considerado"
|
|
)
|
|
|
|
fim_periodo_banco_horas = models.DateField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Fim do período do último banco de horas considerado"
|
|
)
|
|
# Integração externa (opcional)
|
|
matricula_winthor = models.CharField(
|
|
max_length=20,
|
|
null=True,
|
|
blank=True,
|
|
db_index=True,
|
|
help_text="Matrícula/código do usuário correspondente no Winthor"
|
|
)
|
|
|
|
# Controle de sincronização
|
|
sincronizado_em = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Data/hora da última sincronização com o RM"
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = "Pessoa (RM)"
|
|
verbose_name_plural = "Pessoas (RM)"
|
|
|
|
def __str__(self):
|
|
return f"{self.nome} ({self.matricula})"
|
|
#
|
|
class Solicitacao(BaseModel):
|
|
"""
|
|
Entidade central do SGMP e agregador raiz do processo de RH.
|
|
|
|
Uma Solicitação representa um pedido formal que percorre
|
|
um fluxo controlado de aprovação, desde sua criação até
|
|
a finalização ou reprovação.
|
|
|
|
Responsabilidades:
|
|
- Identificar o tipo de processo (ex: desligamento,movimentação, admissão por substuição ou por aumento de quadro)
|
|
- Relacionar solicitante e funcionário
|
|
- Controlar o status atual do fluxo
|
|
- Servir de ponto de ligação para aprovações e dados específicos
|
|
"""
|
|
tipo = models.CharField(
|
|
max_length=30,
|
|
choices=TipoSolicitacao.choices
|
|
)
|
|
|
|
solicitante = models.ForeignKey(
|
|
UsuarioSistema,
|
|
on_delete=models.PROTECT,
|
|
related_name="solicitacoes_criadas"
|
|
)
|
|
|
|
funcionario = models.ForeignKey(
|
|
PessoaRM,
|
|
on_delete=models.PROTECT,
|
|
null=True,
|
|
blank=True,
|
|
related_name="solicitacoes"
|
|
)
|
|
|
|
status = models.CharField(
|
|
max_length=30,
|
|
choices=StatusSolicitacao.choices,
|
|
default=StatusSolicitacao.RASCUNHO
|
|
)
|
|
|
|
enviada_em = models.DateTimeField(null=True, blank=True)
|
|
finalizada_em = models.DateTimeField(null=True, blank=True)
|
|
|
|
def pode_editar(self):
|
|
return self.status == StatusSolicitacao.RASCUNHO
|
|
|
|
def pode_enviar(self):
|
|
return self.status == StatusSolicitacao.RASCUNHO
|
|
|
|
def etapa_atual(self):
|
|
"""
|
|
Retorna a etapa atual baseada no status.
|
|
AGUARDANDO_HEAD: HEAD (apenas Head pode aprovar/reprovar)
|
|
ENVIADA: None (GG e CONTROLADORIA podem dar parecer)
|
|
AGUARDANDO_DIRETORIA: DIRETORIA (apenas Diretoria pode aprovar/reprovar)
|
|
"""
|
|
mapa = {
|
|
StatusSolicitacao.AGUARDANDO_HEAD: EtapaAprovacao.HEAD,
|
|
StatusSolicitacao.ENVIADA: None, # GG e CONTROLADORIA podem dar parecer
|
|
StatusSolicitacao.AGUARDANDO_DIRETORIA: EtapaAprovacao.DIRETORIA,
|
|
}
|
|
return mapa.get(self.status)
|
|
|
|
def pode_aprovar(self, usuario=None):
|
|
"""
|
|
Verifica se a solicitação pode ser aprovada.
|
|
|
|
- HEAD pode aprovar/reprovar quando status é AGUARDANDO_HEAD.
|
|
Considera tanto perfil principal quanto perfis extras.
|
|
- DIRETORIA pode aprovar/reprovar quando status é AGUARDANDO_DIRETORIA.
|
|
Considera tanto perfil principal quanto perfis extras.
|
|
"""
|
|
etapa_atual = self.etapa_atual()
|
|
if etapa_atual is None:
|
|
return False
|
|
|
|
if etapa_atual == EtapaAprovacao.HEAD:
|
|
if usuario and not usuario.tem_perfil(UsuarioSistema.Perfil.HEAD):
|
|
return False
|
|
return True
|
|
if etapa_atual == EtapaAprovacao.DIRETORIA:
|
|
if usuario and not usuario.tem_perfil(UsuarioSistema.Perfil.DIRETORIA):
|
|
return False
|
|
return True
|
|
|
|
return False
|
|
|
|
def pode_dar_parecer(self, usuario):
|
|
"""
|
|
Verifica se o usuário pode dar parecer na solicitação.
|
|
GG e CONTROLADORIA podem dar parecer quando status é ENVIADA.
|
|
"""
|
|
if self.status != StatusSolicitacao.ENVIADA:
|
|
return False
|
|
|
|
# GG e CONTROLADORIA podem dar parecer
|
|
if usuario.perfil not in [UsuarioSistema.Perfil.GG, UsuarioSistema.Perfil.CONTROLADORIA]:
|
|
return False
|
|
|
|
# Verifica se já deu parecer
|
|
etapa_esperada = None
|
|
if usuario.perfil == UsuarioSistema.Perfil.GG:
|
|
etapa_esperada = EtapaAprovacao.GG
|
|
elif usuario.perfil == UsuarioSistema.Perfil.CONTROLADORIA:
|
|
etapa_esperada = EtapaAprovacao.CONTROLADORIA
|
|
|
|
if etapa_esperada:
|
|
parecer_existente = self.pareceres.filter(etapa=etapa_esperada).exists()
|
|
if parecer_existente:
|
|
return False # Já deu parecer
|
|
|
|
return True
|
|
|
|
def __str__(self):
|
|
"""
|
|
Representação textual da solicitação.
|
|
|
|
Em alguns tipos (ex.: Admissão por aumento de quadro) não há
|
|
funcionário vinculado, então protegemos contra None.
|
|
"""
|
|
tipo_label = self.get_tipo_display()
|
|
if self.funcionario:
|
|
return f"{tipo_label} - {self.funcionario.nome}"
|
|
return f"{tipo_label} - (sem funcionário)"
|
|
class TipoDesligamento(models.TextChoices):
|
|
"""Tipos de desligamento suportados pelo sistema."""
|
|
PEDIDO_DEMISSAO = "PEDIDO_DEMISSAO", _("Pedido de Demissão")
|
|
SEM_JUSTA_CAUSA = "SEM_JUSTA_CAUSA", _("Demissão sem Justa Causa")
|
|
COM_JUSTA_CAUSA = "COM_JUSTA_CAUSA", _("Demissão por Justa Causa")
|
|
TERMINO_CONTRATO = "TERMINO_CONTRATO", _("Término de Contrato de Experiência")
|
|
OUTROS = "OUTROS", _("Outros")
|
|
|
|
class TipoAvisoPrevio(models.TextChoices):
|
|
"""Tipos de aviso prévio."""
|
|
TRABALHADO = "TRABALHADO", _("Trabalhado")
|
|
INDENIZADO = "INDENIZADO", _("Indenizado (Dispensa imediata)")
|
|
DISPENSADO = "DISPENSADO", _("Dispensado (Pedido de demissão)")
|
|
|
|
class Desligamento(BaseModel):
|
|
"""
|
|
Modelo que representa os dados específicos de uma solicitação
|
|
de desligamento de funcionário.
|
|
|
|
Está vinculado obrigatoriamente a uma única Solicitação,
|
|
permitindo que o SGMP trate desligamentos como um subtipo
|
|
de processo, sem poluir o modelo genérico de Solicitação.
|
|
|
|
Contém apenas informações relevantes ao processo de desligamento,
|
|
mantendo a separação clara de responsabilidades no domínio.
|
|
"""
|
|
solicitacao = models.OneToOneField(
|
|
Solicitacao,
|
|
on_delete=models.CASCADE,
|
|
related_name="desligamento"
|
|
)
|
|
|
|
tipo_desligamento = models.CharField(
|
|
max_length=30,
|
|
choices=TipoDesligamento.choices,
|
|
help_text="Tipo de desligamento (pedido de demissão, sem justa causa, etc.)",
|
|
null=True,
|
|
blank=True
|
|
)
|
|
|
|
aviso_previo = models.CharField(
|
|
max_length=20,
|
|
choices=TipoAvisoPrevio.choices,
|
|
help_text="Tipo de aviso prévio (trabalhado, indenizado, dispensado)",
|
|
null=True,
|
|
blank=True
|
|
)
|
|
|
|
motivo = models.TextField()
|
|
data_prevista_desligamento = models.DateField()
|
|
observacoes = models.TextField(blank=True)
|
|
|
|
arquivo_pedido = models.FileField(
|
|
upload_to='desligamentos/cartas/%Y/%m/%d/',
|
|
null=True,
|
|
blank=True,
|
|
help_text="Carta de pedido de demissão (obrigatório apenas para pedido de demissão)"
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"Desligamento - {self.solicitacao.funcionario.nome}"
|
|
class AdmissaoSubstituicao(BaseModel):
|
|
"""
|
|
Representa a contratação de um novo colaborador para ocupar a vaga
|
|
de alguém que saiu (desligamento) ou mudou de área (movimentação).
|
|
|
|
Ponto de Atenção: O 'substituído' é a PessoaRM vinculada na Solicitacao pai.
|
|
"""
|
|
solicitacao = models.OneToOneField(
|
|
Solicitacao,
|
|
on_delete=models.CASCADE,
|
|
related_name="admissao_substituicao"
|
|
)
|
|
|
|
data_previsao_contratacao = models.DateField()
|
|
|
|
# Dados do Destino (Onde o novo colaborador irá trabalhar)
|
|
cod_coligada_destino = models.IntegerField(help_text="Cód. Coligada RM")
|
|
cod_filial_destino = models.IntegerField(help_text="Cód. Filial RM")
|
|
cod_secao_destino = models.CharField(max_length=50, help_text="Cód. Seção/Centro de Custo")
|
|
cod_funcao_destino = models.CharField(max_length=50, help_text="Cód. Função/Cargo")
|
|
|
|
justificativa = models.TextField()
|
|
|
|
def __str__(self):
|
|
return f"Substituição de {self.solicitacao.funcionario.nome}"
|
|
|
|
class AdmissaoAumentoQuadro(BaseModel):
|
|
"""
|
|
Representa a abertura de uma nova vaga no orçamento (Headcount),
|
|
sem um colaborador predecessor.
|
|
"""
|
|
solicitacao = models.OneToOneField(
|
|
Solicitacao,
|
|
on_delete=models.CASCADE,
|
|
related_name="admissao_aumento"
|
|
)
|
|
|
|
data_previsao_contratacao = models.DateField()
|
|
justificativa_estrategica = models.TextField()
|
|
|
|
# Estrutura de Destino
|
|
cod_coligada_destino = models.IntegerField()
|
|
cod_filial_destino = models.IntegerField()
|
|
cod_secao_destino = models.CharField(max_length=50)
|
|
cod_funcao_destino = models.CharField(max_length=50)
|
|
|
|
# No BPMN menciona 'Avaliar Suplementação' - campo para controle orçamentário
|
|
requer_suplementacao_orcamentaria = models.BooleanField(default=False)
|
|
|
|
def __str__(self):
|
|
return f"Aumento de Quadro - {self.cod_secao_destino}"
|
|
|
|
class Movimentacao(BaseModel):
|
|
"""
|
|
Representa alteração de cargo, setor ou centro de custo de um
|
|
colaborador ativo.
|
|
"""
|
|
solicitacao = models.OneToOneField(
|
|
Solicitacao,
|
|
on_delete=models.CASCADE,
|
|
related_name="movimentacao"
|
|
)
|
|
|
|
# Flags de Mudança (conforme decisão no BPMN: "Altera Função?", "Transfere CC?")
|
|
altera_funcao = models.BooleanField(default=False)
|
|
altera_centro_custo = models.BooleanField(default=False)
|
|
|
|
# Novos Dados (Snapshot do Destino)
|
|
novo_cod_secao = models.CharField(max_length=50, null=True, blank=True)
|
|
novo_cod_funcao = models.CharField(max_length=50, null=True, blank=True)
|
|
novo_salario = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
|
|
|
data_efetivacao = models.DateField(help_text="Data que a mudança passa a valer no RM")
|
|
justificativa = models.TextField()
|
|
|
|
def __str__(self):
|
|
return f"Movimentação - {self.solicitacao.funcionario.nome}"
|
|
|
|
class Aprovacao(BaseModel):
|
|
"""
|
|
Representa uma decisão formal tomada em uma etapa específica
|
|
do fluxo de aprovação de uma solicitação.
|
|
|
|
Cada aprovação registra:
|
|
- a etapa do processo
|
|
- a decisão tomada
|
|
- o usuário responsável
|
|
- a justificativa
|
|
- o momento da decisão
|
|
|
|
O modelo garante que exista apenas uma aprovação
|
|
por etapa para cada solicitação, assegurando consistência
|
|
e integridade do processo decisório.
|
|
"""
|
|
solicitacao = models.ForeignKey(
|
|
Solicitacao,
|
|
on_delete=models.CASCADE,
|
|
related_name="aprovacoes"
|
|
)
|
|
|
|
etapa = models.CharField(
|
|
max_length=20,
|
|
choices=EtapaAprovacao.choices
|
|
)
|
|
|
|
decisao = models.CharField(
|
|
max_length=10,
|
|
choices=DecisaoAprovacao.choices
|
|
)
|
|
|
|
usuario = models.ForeignKey(
|
|
UsuarioSistema,
|
|
on_delete=models.PROTECT,
|
|
related_name="aprovacoes_realizadas"
|
|
)
|
|
|
|
justificativa = models.TextField()
|
|
decidido_em = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
unique_together = ("solicitacao", "etapa")
|
|
|
|
def __str__(self):
|
|
return f"{self.solicitacao.id} - {self.etapa} - {self.decisao}"
|
|
|
|
class Parecer(BaseModel):
|
|
"""
|
|
Representa um parecer técnico emitido por GG ou CONTROLADORIA
|
|
sobre uma solicitação. Diferente de Aprovacao, um Parecer não
|
|
altera o status da solicitação diretamente, apenas fornece análise e dados.
|
|
|
|
Quando ambos os pareceres (GG e CONTROLADORIA) são dados,
|
|
a solicitação muda para AGUARDANDO_DIRETORIA.
|
|
"""
|
|
solicitacao = models.ForeignKey(
|
|
Solicitacao,
|
|
on_delete=models.CASCADE,
|
|
related_name="pareceres"
|
|
)
|
|
|
|
etapa = models.CharField(
|
|
max_length=20,
|
|
choices=EtapaAprovacao.choices,
|
|
help_text="Etapa do parecer (GG ou CONTROLADORIA)"
|
|
)
|
|
|
|
usuario = models.ForeignKey(
|
|
UsuarioSistema,
|
|
on_delete=models.PROTECT,
|
|
related_name="pareceres_emitidos"
|
|
)
|
|
|
|
texto = models.TextField(
|
|
help_text="Análise, dados e considerações sobre a solicitação"
|
|
)
|
|
|
|
anexo = models.FileField(
|
|
upload_to='pareceres/anexos/%Y/%m/%d/',
|
|
null=True,
|
|
blank=True,
|
|
help_text="Anexo opcional ao parecer (documentos, planilhas, etc.)"
|
|
)
|
|
|
|
criado_em = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
unique_together = ("solicitacao", "etapa")
|
|
verbose_name = "Parecer"
|
|
verbose_name_plural = "Pareceres"
|
|
|
|
def __str__(self):
|
|
return f"Parecer {self.etapa} - {self.solicitacao.id}"
|
|
|