portal_publico/portal_api/serializers.py

1276 lines
53 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import Any
import nh3
from django.db.models import Q
from rest_framework import serializers
from . import catalogo
from .indicadores import calculo as indicadores_calculo
from .models import (
AcessoGeral,
AcessoGeralSecao,
CategoriaEvento,
CompromissoAgenda,
Departamento,
Favorito,
FuncaoTelefonia,
ImportacaoPlanoSaude,
ImportacaoPlanoSaudeAlteracao,
ImportacaoPlanoSaudeAuditoria,
ImportacaoPlanoSaudeLinha,
IndicadorApuracao,
IndicadorApuracaoColaborador,
IndicadorApuracaoEmpresa,
IndicadorApuracaoResposta,
IndicadorCriterio,
IndicadorDepartamento,
IndicadorDepartamentoGerente,
IndicadorPercentualTipo,
LinkFerramenta,
LinkFerramentaFavorito,
NotificacaoDispensada,
ParametroFiscalCustoContratacao,
PerfilAcesso,
Ramal,
RamalAusencia,
RegraCusteioPlanoSaude,
TelefoneExterno,
Usuario,
WidgetUsuario,
)
from .custo_contratacao.calculo import EntradaSimulacaoEmpregado
from .planos_saude.leiaute_sistema import formata_valor_br, parse_valor_br
from .planos_saude.pipeline import CUSTEIOS_VALIDOS, OPERADORAS, TIPOS_LANCAMENTO_VALIDOS, TIPOS_PESSOA_VALIDOS
from .planos_saude.regras_empresa import REGRAS_EMPRESA
# Observações de Acesso Geral aceitam imagens embutidas (ver "Acessos Gerais" no
# CLAUDE.md) — o conteúdo vem de um <div contenteditable> no cliente e é
# renderizado via innerHTML pra qualquer usuário com acesso de visualização, então
# passa por um allowlist estrito (nh3 — binding Python da lib Rust "ammonia";
# usado no lugar do bleach, que está sem manutenção desde 2023 e parou de
# receber releases, inclusive de segurança) antes de salvar: só marcação de
# texto básica + <img>, nada de <script>/<a>/atributos de evento. `data` entra
# nos esquemas de URL aceitos porque as imagens inseridas viajam como data URI
# (sem upload de arquivo separado), não como URL externa.
ACESSO_GERAL_OBSERVACOES_ALLOWED_TAGS = {"p", "br", "div", "b", "strong", "i", "em", "u", "ul", "ol", "li", "img"}
ACESSO_GERAL_OBSERVACOES_ALLOWED_ATTRS = {"img": {"src", "alt"}}
ACESSO_GERAL_OBSERVACOES_ALLOWED_SCHEMES = {"http", "https", "data"}
class DepartamentoSerializer(serializers.ModelSerializer):
class Meta:
model = Departamento
fields = ["id", "nome"]
class PerfilAcessoSerializer(serializers.ModelSerializer):
class Meta:
model = PerfilAcesso
fields = ["codigo", "nome", "ativo", "gerencia_permissoes", "permissoes"]
read_only_fields = ["codigo"]
def validate_permissoes(self, value: dict[str, Any]) -> dict[str, Any]:
if not isinstance(value, dict):
raise serializers.ValidationError("Permissões devem ser um objeto.")
modulos_validos = {m["key"] for m in catalogo.MODULES}
for chave in value:
if chave not in modulos_validos:
raise serializers.ValidationError(f"Módulo desconhecido: {chave}")
return value
class PerfilResumoSerializer(serializers.ModelSerializer):
class Meta:
model = PerfilAcesso
fields = ["codigo", "nome", "gerencia_permissoes"]
class UsuarioResumoSerializer(serializers.ModelSerializer):
"""Representação enxuta (id/nome/departamentos) de um usuário — usada nos dois
sentidos da relação de liderança (`liderados` expandido em
`UsuarioListSerializer`/`/api/me/`) e por `/api/usuarios-resumo/`, que alimenta o
seletor de liderados (com busca e coluna por departamento) sem exigir
`gerencia_permissoes` (mesmo padrão de `RamalViewSet.usuarios_disponiveis`)."""
departamentos = DepartamentoSerializer(many=True, read_only=True)
class Meta:
model = Usuario
fields = ["id", "nome", "departamentos"]
class UsuarioSerializer(serializers.ModelSerializer):
"""Usado para criar/editar (aceita `senha` e `perfis` graváveis)."""
perfis = serializers.PrimaryKeyRelatedField(queryset=PerfilAcesso.objects.all(), many=True, required=False)
departamentos = serializers.PrimaryKeyRelatedField(
queryset=Departamento.objects.all(), many=True, required=False
)
liderados = serializers.PrimaryKeyRelatedField(queryset=Usuario.objects.all(), many=True, required=False)
senha = serializers.CharField(write_only=True, required=False, allow_blank=True)
class Meta:
model = Usuario
fields = [
"id",
"username",
"nome",
"email",
"departamentos",
"codigo_folha",
"codigo_questor",
"codigo_tareffa",
"codigo_contabit",
"ramal",
"data_aniversario",
"perfis",
"lideranca",
"liderados",
"senha",
"is_active",
]
def create(self, validated_data: dict[str, Any]) -> Usuario:
senha = validated_data.pop("senha", None)
perfis = validated_data.pop("perfis", [])
departamentos = validated_data.pop("departamentos", [])
liderados = validated_data.pop("liderados", [])
if not senha:
raise serializers.ValidationError({"senha": "Informe uma senha para o novo usuário."})
usuario = Usuario(**validated_data)
usuario.set_password(senha)
usuario.save()
usuario.perfis.set(perfis)
usuario.departamentos.set(departamentos)
usuario.liderados.set(liderados)
return usuario
def update(self, instance: Usuario, validated_data: dict[str, Any]) -> Usuario:
senha = validated_data.pop("senha", None)
perfis = validated_data.pop("perfis", None)
departamentos = validated_data.pop("departamentos", None)
liderados = validated_data.pop("liderados", None)
for attr, value in validated_data.items():
setattr(instance, attr, value)
if senha:
instance.set_password(senha)
instance.save()
if perfis is not None:
instance.perfis.set(perfis)
if departamentos is not None:
instance.departamentos.set(departamentos)
if liderados is not None:
instance.liderados.set(liderados)
return instance
class UsuarioListSerializer(serializers.ModelSerializer):
"""Usado para listar/detalhar (perfis e departamentos expandidos, somente leitura)."""
perfis = PerfilResumoSerializer(many=True, read_only=True)
departamentos = DepartamentoSerializer(many=True, read_only=True)
liderados = UsuarioResumoSerializer(many=True, read_only=True)
class Meta:
model = Usuario
fields = [
"id",
"username",
"nome",
"email",
"departamentos",
"codigo_folha",
"codigo_questor",
"codigo_tareffa",
"codigo_contabit",
"ramal",
"data_aniversario",
"perfis",
"lideranca",
"liderados",
"is_active",
]
class CategoriaEventoSerializer(serializers.ModelSerializer):
class Meta:
model = CategoriaEvento
fields = ["id", "nome", "cor"]
class CompromissoAgendaSerializer(serializers.ModelSerializer):
dono_nome = serializers.CharField(source="dono.nome", read_only=True)
dono_username = serializers.CharField(source="dono.username", read_only=True)
departamento_compartilhado_nome = serializers.CharField(
source="departamento_compartilhado.nome", read_only=True, default=None
)
categoria_nome = serializers.CharField(source="categoria.nome", read_only=True, default=None)
categoria_cor = serializers.CharField(source="categoria.cor", read_only=True, default=None)
sou_dono = serializers.SerializerMethodField()
notificar_em = serializers.SerializerMethodField()
class Meta:
model = CompromissoAgenda
fields = [
"id",
"titulo",
"data",
"horario",
"visibilidade",
"departamento_compartilhado",
"departamento_compartilhado_nome",
"lembrete_antecedencia",
"categoria",
"categoria_nome",
"categoria_cor",
"eh_evento",
"local",
"modalidade",
"descricao",
"notificar_em",
"dono_nome",
"dono_username",
"sou_dono",
]
def get_sou_dono(self, obj: CompromissoAgenda) -> bool:
request = self.context.get("request")
return bool(request and request.user.is_authenticated and obj.dono_id == request.user.id)
def get_notificar_em(self, obj: CompromissoAgenda) -> str | None:
notificar_em = obj.calcular_notificar_em()
return notificar_em.isoformat() if notificar_em else None
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
instance = self.instance
# Evento (`eh_evento`) é sempre visível pra todo mundo, por definição —
# força a visibilidade antes de qualquer outra checagem, então o gate de
# permissão abaixo (que já cobre "departamento"/"todos") também cobre
# a criação de eventos sem precisar de uma checagem duplicada.
eh_evento = attrs.get("eh_evento", getattr(instance, "eh_evento", False))
if eh_evento:
attrs["visibilidade"] = CompromissoAgenda.VISIBILIDADE_TODOS
visibilidade = attrs.get(
"visibilidade", getattr(instance, "visibilidade", CompromissoAgenda.VISIBILIDADE_SOMENTE_EU)
)
if visibilidade == CompromissoAgenda.VISIBILIDADE_DEPARTAMENTO:
if "departamento_compartilhado" in attrs:
departamento = attrs["departamento_compartilhado"]
else:
departamento = getattr(instance, "departamento_compartilhado", None)
request = self.context.get("request")
if departamento is None:
raise serializers.ValidationError(
{"departamento_compartilhado": "Escolha um departamento para compartilhar o compromisso."}
)
if request and not request.user.departamentos.filter(pk=departamento.pk).exists():
raise serializers.ValidationError(
{"departamento_compartilhado": "Escolha um dos seus próprios departamentos."}
)
attrs["departamento_compartilhado"] = departamento
else:
attrs["departamento_compartilhado"] = None
if visibilidade in (CompromissoAgenda.VISIBILIDADE_DEPARTAMENTO, CompromissoAgenda.VISIBILIDADE_TODOS):
request = self.context.get("request")
if not (
request
and request.user.permissao_app("calendario-individual", "calendario-individual-criar-evento")
):
raise serializers.ValidationError(
{"visibilidade": "Você não tem permissão para criar eventos de departamento ou para todos."}
)
return attrs
class FavoritoSerializer(serializers.ModelSerializer):
class Meta:
model = Favorito
fields = ["id", "app_id", "ordem"]
class NotificacaoDispensadaSerializer(serializers.ModelSerializer):
class Meta:
model = NotificacaoDispensada
fields = ["id", "notif_id"]
class WidgetUsuarioSerializer(serializers.ModelSerializer):
class Meta:
model = WidgetUsuario
fields = ["id", "tipo", "ordem", "largura", "altura"]
class LinkFerramentaSerializer(serializers.ModelSerializer):
class Meta:
model = LinkFerramenta
fields = ["id", "nome", "url", "icone", "ordem"]
class LinkFerramentaFavoritoSerializer(serializers.ModelSerializer):
link = serializers.PrimaryKeyRelatedField(queryset=LinkFerramenta.objects.all())
class Meta:
model = LinkFerramentaFavorito
fields = ["id", "link"]
class AcessoGeralSecaoSerializer(serializers.ModelSerializer):
"""`perfis_restritos` vazio = seção visível a qualquer um com
`acessos-gerais-visualizar`; não vazio = só quem tem um desses perfis também
(ver `AcessoGeralSecaoViewSet.get_queryset`)."""
perfis_restritos = serializers.PrimaryKeyRelatedField(
queryset=PerfilAcesso.objects.all(), many=True, required=False
)
class Meta:
model = AcessoGeralSecao
fields = ["id", "nome", "ordem", "perfis_restritos"]
def validate_nome(self, value: str) -> str:
if not value.strip():
raise serializers.ValidationError("Informe o nome da seção.")
return value
class AcessoGeralSerializer(serializers.ModelSerializer):
class Meta:
model = AcessoGeral
fields = ["id", "secao", "nome", "url", "usuario", "senha", "observacoes", "ordem"]
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
request = self.context.get("request")
if request is not None and request.user.is_authenticated:
# Restringe o <select> de seção às que o usuário pode ver (mesmo filtro
# de AcessoGeralSecaoViewSet.get_queryset), senão daria pra criar/mover
# uma linha pra dentro de uma seção restrita a outro perfil só sabendo o id.
perfis_ids = list(request.user.perfis.values_list("codigo", flat=True))
self.fields["secao"].queryset = AcessoGeralSecao.objects.filter(
Q(perfis_restritos__isnull=True) | Q(perfis_restritos__codigo__in=perfis_ids)
).distinct()
def validate_nome(self, value: str) -> str:
if not value.strip():
raise serializers.ValidationError("Informe o nome do acesso.")
return value
def validate_observacoes(self, value: str) -> str:
return nh3.clean(
value,
tags=ACESSO_GERAL_OBSERVACOES_ALLOWED_TAGS,
attributes=ACESSO_GERAL_OBSERVACOES_ALLOWED_ATTRS,
url_schemes=ACESSO_GERAL_OBSERVACOES_ALLOWED_SCHEMES,
)
class RamalAusenciaSerializer(serializers.ModelSerializer):
usuario_nome = serializers.CharField(source="usuario.nome", read_only=True)
class Meta:
model = RamalAusencia
fields = [
"id",
"usuario",
"usuario_nome",
"data_inicio",
"hora_inicio",
"data_fim",
"hora_volta",
"tipo",
"observacoes",
"encerrada_manualmente",
]
class RamalSerializer(serializers.ModelSerializer):
"""Só para as linhas avulsas (sem `Usuario` por trás) — colaboradores com conta
aparecem automaticamente na listagem via `RamalViewSet.list`, que monta a linha
deles direto do cadastro e não passa por este serializer."""
class Meta:
model = Ramal
fields = ["id", "nome", "departamento", "numero", "criado_em"]
def validate_nome(self, value: str) -> str:
if not value.strip():
raise serializers.ValidationError("Informe o nome.")
return value
class TelefoneExternoSerializer(serializers.ModelSerializer):
class Meta:
model = TelefoneExterno
fields = ["id", "nome", "ramal", "telefone", "observacoes", "criado_em"]
def validate_nome(self, value: str) -> str:
if not value.strip():
raise serializers.ValidationError("Informe o nome.")
return value
class FuncaoTelefoniaSerializer(serializers.ModelSerializer):
class Meta:
model = FuncaoTelefonia
fields = ["id", "comando", "funcao", "resumo", "criado_em"]
def validate_comando(self, value: str) -> str:
if not value.strip():
raise serializers.ValidationError("Informe o comando.")
return value
def _monta_regra_custeio(chave: str, modo: str | None, limite_bruto: str, percentual_bruto: str) -> dict[str, Any]:
"""Valida e monta uma única regra de custeio (uma combinação tipo de
lançamento × tipo de beneficiário) — reaproveitado por
ImportacaoPlanoSaudeCreateSerializer (form da nova importação) e
RegraCusteioPlanoSaudeSerializer (banco de regras salvas), já que as duas
telas usam exatamente a mesma regra de negócio de custeio (ver "Regra de
custeio" no formulário de nova importação)."""
if not modo:
raise serializers.ValidationError(
{f"custeio_{chave}": "Informe como esse tipo é custeado para titular e dependente."}
)
if modo not in CUSTEIOS_VALIDOS:
raise serializers.ValidationError({f"custeio_{chave}": f"Modo de custeio inválido: {modo}"})
if modo != "especifica":
return {"modo": modo}
limite_bruto = (limite_bruto or "").strip()
percentual_bruto = (percentual_bruto or "").strip()
if not limite_bruto and not percentual_bruto:
raise serializers.ValidationError(
{f"custeio_{chave}": "Informe o limite de valor e/ou o percentual de custeio da empresa."}
)
regra: dict[str, Any] = {"modo": "especifica", "limite_valor": None, "percentual": None}
if limite_bruto:
limite = parse_valor_br(limite_bruto)
if limite < 0:
raise serializers.ValidationError({f"limite_valor_{chave}": "Informe um valor válido."})
regra["limite_valor"] = limite
if percentual_bruto:
percentual = parse_valor_br(percentual_bruto)
if not (0 <= percentual <= 100):
raise serializers.ValidationError({f"percentual_{chave}": "Informe um percentual entre 0 e 100."})
regra["percentual"] = percentual
return regra
def _valor_custeio_para_texto_br(valor: Any) -> str:
"""`RegraCusteioPlanoSaudeSerializer` recebe `limite_valor`/`percentual`
tanto como texto BR cru (regra recém-preenchida no formulário) quanto já
como float (regra salva sendo reaplicada sem edição, vinda de volta do
próprio `custeio_por_tipo` já persistido) — normaliza os dois formatos
antes de `_monta_regra_custeio` chamar `parse_valor_br`, que só entende
texto BR (`"150.0"` seria lido errado como 15000, não 150)."""
if valor is None or valor == "":
return ""
if isinstance(valor, (int, float)):
return formata_valor_br(float(valor))
return str(valor)
class ImportacaoPlanoSaudeCreateSerializer(serializers.Serializer):
"""Valida o multipart de criação de uma nova importação — não é um
ModelSerializer porque o formato de entrada (`tipos_lancamento` como string
separada por vírgula + um `custeio_<tipo>_<pessoa>` por tipo de lançamento
× tipo de beneficiário) não bate 1:1 com os campos do model
(`ImportacaoPlanoSaude.custeio_por_tipo` é um único JSONField, no formato
`{tipo: {"titular": {...regra...}, "dependente": {...regra...}}}`). Ver
`ImportacaoPlanoSaudeViewSet.create` em views.py."""
operadora = serializers.ChoiceField(choices=list(OPERADORAS.keys()))
planilha_padrao = serializers.FileField()
arquivo_operadora = serializers.FileField()
tipos_lancamento = serializers.CharField()
# Chave de REGRAS_EMPRESA (portal_api.planos_saude.regras_empresa) — ver
# validate(): quando preenchida, substitui o custeio manual de
# "mensalidade" (custeio_mensalidade_titular/dependente ficam ignorados).
regra_empresa = serializers.CharField(required=False, allow_blank=True)
custeio_mensalidade_titular = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
custeio_mensalidade_dependente = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
custeio_coparticipacao_titular = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
custeio_coparticipacao_dependente = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
# Só usados quando custeio_<tipo>_<pessoa> == "especifica" (ver
# validate()) — texto livre porque chegam no formato BR ("150,00"/"50")
# do formulário, igual aos outros valores monetários do pipeline.
limite_valor_mensalidade_titular = serializers.CharField(required=False, allow_blank=True)
percentual_mensalidade_titular = serializers.CharField(required=False, allow_blank=True)
limite_valor_mensalidade_dependente = serializers.CharField(required=False, allow_blank=True)
percentual_mensalidade_dependente = serializers.CharField(required=False, allow_blank=True)
limite_valor_coparticipacao_titular = serializers.CharField(required=False, allow_blank=True)
percentual_coparticipacao_titular = serializers.CharField(required=False, allow_blank=True)
limite_valor_coparticipacao_dependente = serializers.CharField(required=False, allow_blank=True)
percentual_coparticipacao_dependente = serializers.CharField(required=False, allow_blank=True)
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
tipos = [t.strip() for t in attrs["tipos_lancamento"].split(",") if t.strip()]
if not tipos:
raise serializers.ValidationError({"tipos_lancamento": "Selecione ao menos um tipo de importação."})
regra_empresa_key = (attrs.get("regra_empresa") or "").strip()
if regra_empresa_key:
if regra_empresa_key not in REGRAS_EMPRESA:
raise serializers.ValidationError({"regra_empresa": f"Regra empresa desconhecida: {regra_empresa_key}"})
if "mensalidade" not in tipos:
raise serializers.ValidationError(
{"regra_empresa": "'Regra empresa' exige o tipo de importação 'Mensalidade' selecionado."}
)
parser_classe = OPERADORAS[attrs["operadora"]]["parser"]
if parser_classe.chave_casamento != "nome":
raise serializers.ValidationError(
{"regra_empresa": "Esta operadora não é compatível com 'Regra empresa' (requer casamento por nome)."}
)
custeio_por_tipo: dict[str, Any] = {}
for tipo in tipos:
if tipo not in TIPOS_LANCAMENTO_VALIDOS:
raise serializers.ValidationError({"tipos_lancamento": f"Tipo de importação inválido: {tipo}"})
# "Regra empresa" substitui o custeio manual de mensalidade —
# sem titular/dependente pra configurar aqui (ver models.py
# ImportacaoPlanoSaude.regra_empresa).
if tipo == "mensalidade" and regra_empresa_key:
custeio_por_tipo[tipo] = {}
continue
custeio_por_pessoa: dict[str, Any] = {}
for pessoa in TIPOS_PESSOA_VALIDOS:
chave = f"{tipo}_{pessoa}"
custeio_por_pessoa[pessoa] = _monta_regra_custeio(
chave,
attrs.get(f"custeio_{chave}"),
attrs.get(f"limite_valor_{chave}", ""),
attrs.get(f"percentual_{chave}", ""),
)
custeio_por_tipo[tipo] = custeio_por_pessoa
attrs["tipos_lancamento_lista"] = tipos
attrs["custeio_por_tipo"] = custeio_por_tipo
attrs["regra_empresa"] = regra_empresa_key
return attrs
class ImportacaoPlanoSaudeLinhaSerializer(serializers.ModelSerializer):
class Meta:
model = ImportacaoPlanoSaudeLinha
fields = [
"id",
"tipo_lancamento",
"codigo_empresa",
"nome_func",
"cpf_func",
"codigo_out_emp",
"data_inicial",
"nome_dependente",
"cpf_dependente",
"valor_empresa",
"valor",
"descricao",
]
read_only_fields = ["id", "tipo_lancamento"]
class ImportacaoPlanoSaudeLinhaCreateSerializer(serializers.ModelSerializer):
"""Só para incluir manualmente uma linha nova numa importação já criada
(botão "Adicionar linha" na revisão) — ao contrário de
ImportacaoPlanoSaudeLinhaSerializer (edição), aqui `importacao`/
`tipo_lancamento` são graváveis, já que precisam ser informados na
criação. Os demais campos nascem em branco/"0" (mesmo default do model)
e são editados depois pelos mesmos inputs das linhas geradas pelo
pipeline."""
class Meta:
model = ImportacaoPlanoSaudeLinha
fields = [
"id",
"importacao",
"tipo_lancamento",
"codigo_empresa",
"nome_func",
"cpf_func",
"codigo_out_emp",
"data_inicial",
"nome_dependente",
"cpf_dependente",
"valor_empresa",
"valor",
"descricao",
]
read_only_fields = ["id"]
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
importacao = attrs.get("importacao")
tipo = attrs.get("tipo_lancamento")
if tipo not in TIPOS_LANCAMENTO_VALIDOS:
raise serializers.ValidationError({"tipo_lancamento": f"Tipo de importação inválido: {tipo}"})
if importacao is not None and tipo not in (importacao.tipos_lancamento or []):
raise serializers.ValidationError(
{"tipo_lancamento": "Esse tipo de lançamento não faz parte desta importação."}
)
return attrs
class ImportacaoPlanoSaudeAuditoriaSerializer(serializers.ModelSerializer):
linha_vinculada_nome = serializers.SerializerMethodField()
class Meta:
model = ImportacaoPlanoSaudeAuditoria
fields = [
"id",
"motivo",
"tipo_lancamento",
"numero_beneficiario",
"nome",
"cpf",
"tipo",
"valor",
"detalhe",
"resolvida",
"linha_vinculada",
"linha_vinculada_nome",
]
read_only_fields = ["resolvida", "linha_vinculada"]
def get_linha_vinculada_nome(self, obj: ImportacaoPlanoSaudeAuditoria) -> str | None:
"""Nome pra exibir junto do selo "Resolvido" — o nome do dependente
se a linha vinculada for uma linha de dependente, senão o do titular."""
if not obj.linha_vinculada:
return None
return obj.linha_vinculada.nome_dependente or obj.linha_vinculada.nome_func
class ImportacaoPlanoSaudeAlteracaoSerializer(serializers.ModelSerializer):
usuario_nome = serializers.CharField(source="usuario.nome", read_only=True, default=None)
linha_nome = serializers.SerializerMethodField()
class Meta:
model = ImportacaoPlanoSaudeAlteracao
fields = [
"id",
"tipo",
"linha",
"linha_nome",
"tipo_lancamento",
"campo",
"valor_anterior",
"valor_novo",
"dados_linha",
"usuario_nome",
"criado_em",
"revertida",
"revertida_em",
]
read_only_fields = fields
def get_linha_nome(self, obj: ImportacaoPlanoSaudeAlteracao) -> str | None:
"""Nome pra identificar a linha na aba "Alterações" mesmo quando ela já
não existe mais (excluída, ou uma inclusão já revertida) — usa o
snapshot em `dados_linha` nesse caso, senão o estado atual da própria
linha (que pode ter sido editada depois desta alteração específica)."""
dados = obj.dados_linha or {}
if obj.linha_id:
return obj.linha.nome_dependente or obj.linha.nome_func or None
return dados.get("nome_dependente") or dados.get("nome_func") or None
class ImportacaoPlanoSaudeListSerializer(serializers.ModelSerializer):
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
codigo_empresa = serializers.SerializerMethodField()
class Meta:
model = ImportacaoPlanoSaude
fields = [
"id",
"nome_operadora",
"codigo_empresa",
"tipos_lancamento",
"custeio_por_tipo",
"status",
"criado_por_nome",
"criado_em",
"concluida_em",
]
def get_codigo_empresa(self, obj: ImportacaoPlanoSaude) -> str:
"""Todas as linhas de uma importação vêm da mesma planilha padrão, então
compartilham o mesmo código de empresa — pega o primeiro não vazio (uma
linha incluída manualmente pode nascer sem esse campo preenchido)."""
linha = obj.linhas.exclude(codigo_empresa="").first()
return linha.codigo_empresa if linha else ""
class ImportacaoPlanoSaudeDetailSerializer(serializers.ModelSerializer):
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
linhas = ImportacaoPlanoSaudeLinhaSerializer(many=True, read_only=True)
itens_auditoria = ImportacaoPlanoSaudeAuditoriaSerializer(many=True, read_only=True)
alteracoes = ImportacaoPlanoSaudeAlteracaoSerializer(many=True, read_only=True)
resumo_por_tipo = serializers.SerializerMethodField()
regra_empresa_label = serializers.SerializerMethodField()
class Meta:
model = ImportacaoPlanoSaude
fields = [
"id",
"operadora",
"nome_operadora",
"tipos_lancamento",
"custeio_por_tipo",
"regra_empresa",
"regra_empresa_label",
"status",
"criado_por_nome",
"criado_em",
"concluida_em",
"linhas",
"itens_auditoria",
"alteracoes",
"resumo_por_tipo",
]
def get_regra_empresa_label(self, obj: ImportacaoPlanoSaude) -> str | None:
"""Rótulo pra exibir na revisão (ex.: "1778 - Unimed (Tecnomyl)") —
REGRAS_EMPRESA é um registro fixo no código, não uma tabela, então
não dá pra expor via FK/serializer aninhado normal."""
regra = REGRAS_EMPRESA.get(obj.regra_empresa)
return regra["label"] if regra else None
def get_resumo_por_tipo(self, obj: ImportacaoPlanoSaude) -> list[dict[str, Any]]:
"""Contagem de apoio pra tela de revisão: quantas linhas do tipo têm valor
lançado (VALOR ou VALOREMPRESA diferente de "0") vs. quantas ficaram em
auditoria — mesmos números que o CLI original imprimia no fim (main.py).
`total_auditoria` só conta itens ainda pendentes — um item resolvido
manualmente (ver ImportacaoPlanoSaudeAuditoriaViewSet.resolver) já tem
seu valor refletido em `total_lancadas` (a linha vinculada passa a ter
valor != "0"), então contá-lo de novo aqui inflaria os dois números."""
resumo = []
for tipo in obj.tipos_lancamento:
linhas_do_tipo = [l for l in obj.linhas.all() if l.tipo_lancamento == tipo]
lancadas = sum(1 for l in linhas_do_tipo if l.valor != "0" or l.valor_empresa != "0")
auditoria_do_tipo = sum(
1 for item in obj.itens_auditoria.all() if item.tipo_lancamento == tipo and not item.resolvida
)
resumo.append({
"tipo_lancamento": tipo,
"total_linhas": len(linhas_do_tipo),
"total_lancadas": lancadas,
"total_auditoria": auditoria_do_tipo,
})
return resumo
class RegraCusteioPlanoSaudeSerializer(serializers.ModelSerializer):
"""Banco de regras de custeio salvas (ex.: "092 - Unimed") — substitui o
antigo fluxo de exportar/importar um arquivo `.json`. `custeio_por_tipo`
chega do frontend no mesmo "formato de formulário" já usado por
coletarRegraCusteioAtual() (limite_valor/percentual como texto BR, ex.
"150,00") e é validado/normalizado aqui pro formato final (float|None)
antes de salvar, via `_monta_regra_custeio` — mesma regra de negócio de
ImportacaoPlanoSaudeCreateSerializer, pra nunca divergir entre os dois
pontos de entrada."""
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
class Meta:
model = RegraCusteioPlanoSaude
fields = [
"id",
"nome",
"operadora",
"tipos_lancamento",
"custeio_por_tipo",
"observacoes",
"criado_por_nome",
"criado_em",
"atualizado_em",
]
read_only_fields = ["id", "criado_por_nome", "criado_em", "atualizado_em"]
def validate_nome(self, value: str) -> str:
value = value.strip()
if not value:
raise serializers.ValidationError("Informe um nome para a regra.")
return value
def validate_tipos_lancamento(self, value: Any) -> list[str]:
if not isinstance(value, list) or not value:
raise serializers.ValidationError("Selecione ao menos um tipo de importação.")
for tipo in value:
if tipo not in TIPOS_LANCAMENTO_VALIDOS:
raise serializers.ValidationError(f"Tipo de importação inválido: {tipo}")
return value
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
tipos = attrs.get("tipos_lancamento")
if tipos is None:
tipos = self.instance.tipos_lancamento if self.instance else []
custeio_bruto = attrs.get("custeio_por_tipo")
if custeio_bruto is None:
custeio_bruto = self.instance.custeio_por_tipo if self.instance else {}
if not isinstance(custeio_bruto, dict):
raise serializers.ValidationError({"custeio_por_tipo": "Formato inválido."})
custeio_validado: dict[str, Any] = {}
for tipo in tipos:
por_tipo = custeio_bruto.get(tipo) or {}
if not isinstance(por_tipo, dict):
raise serializers.ValidationError({"custeio_por_tipo": f"Formato inválido para o tipo {tipo}."})
custeio_por_pessoa: dict[str, Any] = {}
for pessoa in TIPOS_PESSOA_VALIDOS:
chave = f"{tipo}_{pessoa}"
entrada = por_tipo.get(pessoa) or {}
if not isinstance(entrada, dict):
entrada = {}
custeio_por_pessoa[pessoa] = _monta_regra_custeio(
chave,
entrada.get("modo"),
_valor_custeio_para_texto_br(entrada.get("limite_valor")),
_valor_custeio_para_texto_br(entrada.get("percentual")),
)
custeio_validado[tipo] = custeio_por_pessoa
attrs["custeio_por_tipo"] = custeio_validado
return attrs
class IndicadorPercentualTipoSerializer(serializers.ModelSerializer):
tipo_label = serializers.CharField(source="get_tipo_display", read_only=True)
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
departamento_nome = serializers.CharField(source="departamento.nome", read_only=True)
class Meta:
model = IndicadorPercentualTipo
fields = [
"id",
"departamento",
"departamento_nome",
"tipo",
"tipo_label",
"percentual_individual",
"percentual_grupo",
"percentual_departamento",
"vigente_desde",
"criado_por_nome",
"criado_em",
]
read_only_fields = ["id", "criado_em"]
class IndicadorCriterioSerializer(serializers.ModelSerializer):
departamento_nome = serializers.CharField(source="departamento.nome", read_only=True)
class Meta:
model = IndicadorCriterio
fields = [
"id",
"departamento",
"departamento_nome",
"nome",
"grupo",
"peso",
"periodo",
"papel_aplicavel",
"calculo_automatico",
"limiar_percentual",
"ativo",
"criado_em",
]
read_only_fields = ["id", "criado_em"]
class IndicadorApuracaoCreateSerializer(serializers.Serializer):
"""Entrada de "Nova Apuração" (multipart): mês/ano de competência + as duas
planilhas mensais (Serviços Tareffa e Honorários Por Cliente)."""
competencia = serializers.DateField()
planilha_tareffa = serializers.FileField()
planilha_honorarios = serializers.FileField()
class IndicadorApuracaoEmpresaSerializer(serializers.ModelSerializer):
tipo_label = serializers.CharField(source="get_tipo_display", read_only=True)
class Meta:
model = IndicadorApuracaoEmpresa
fields = [
"id",
"codigo_empresa",
"nome_empresa",
"honorario",
"honorario_ajustado",
"honorario_nao_encontrado",
"honorario_ajustado_manualmente",
"tipo",
"tipo_label",
"valor_individual",
"valor_grupo",
"valor_departamento",
"valor_total",
]
read_only_fields = fields
class IndicadorApuracaoEmpresaAjusteSerializer(serializers.Serializer):
"""Preenchimento manual do honorário de uma empresa com
`honorario_nao_encontrado=True` — ver `IndicadorApuracaoEmpresaViewSet`."""
honorario = serializers.DecimalField(max_digits=12, decimal_places=2)
class IndicadorApuracaoEmpresaTrocarResponsavelSerializer(serializers.Serializer):
"""Reatribui uma linha (empresa+tipo) pra outro colaborador da mesma
apuração — ver `IndicadorApuracaoEmpresaViewSet.trocar_responsavel`."""
colaborador_id = serializers.IntegerField()
class IndicadorApuracaoRespostaSerializer(serializers.ModelSerializer):
criterio_nome = serializers.CharField(source="criterio.nome", read_only=True)
criterio_grupo = serializers.CharField(source="criterio.grupo", read_only=True)
criterio_peso = serializers.DecimalField(source="criterio.peso", read_only=True, max_digits=7, decimal_places=4)
criterio_periodo = serializers.CharField(source="criterio.periodo", read_only=True)
criterio_papel_aplicavel = serializers.CharField(source="criterio.papel_aplicavel", read_only=True)
class Meta:
model = IndicadorApuracaoResposta
fields = [
"id",
"colaborador",
"criterio",
"criterio_nome",
"criterio_grupo",
"criterio_peso",
"criterio_periodo",
"criterio_papel_aplicavel",
"valor",
"valor_automatico",
"percentual_calculado",
"ajustado_manualmente",
]
read_only_fields = ["id", "colaborador", "criterio", "valor_automatico", "percentual_calculado"]
def update(self, instance: IndicadorApuracaoResposta, validated_data: dict[str, Any]) -> IndicadorApuracaoResposta:
validated_data["ajustado_manualmente"] = True
return super().update(instance, validated_data)
class IndicadorApuracaoRespostaLoteSerializer(serializers.Serializer):
resposta_ids = serializers.ListField(child=serializers.IntegerField(), allow_empty=False)
valor = serializers.ChoiceField(choices=["SIM", "NAO", "NAO_FAZ", "NAO_SE_APLICA"])
class IndicadorApuracaoColaboradorSerializer(serializers.ModelSerializer):
empresas = IndicadorApuracaoEmpresaSerializer(many=True, read_only=True)
respostas = IndicadorApuracaoRespostaSerializer(many=True, read_only=True)
composicao_individual = serializers.SerializerMethodField()
# `departamento.nome` com fallback None — colaborador cujo gerente ainda
# não está mapeado a nenhum departamento (ver IndicadorDepartamentoGerente)
# fica com `departamento` nulo; mesmo padrão de `criado_por_nome` acima.
departamento_nome = serializers.CharField(source="departamento.nome", read_only=True, default=None)
class Meta:
model = IndicadorApuracaoColaborador
fields = [
"id",
"nome",
"gerente",
"departamento",
"departamento_nome",
"pct_individual",
"pct_individual_ajustado_manualmente",
"pct_grupo",
"pct_grupo_ajustado_manualmente",
"pct_departamento",
"pct_departamento_ajustado_manualmente",
"valor_total",
"validado",
"empresas",
"respostas",
"composicao_individual",
]
read_only_fields = fields
def get_composicao_individual(self, obj: IndicadorApuracaoColaborador) -> dict[str, Any]:
"""Detalhamento de como o "Total Indicador" (`pct_individual`) foi
composto a partir dos 3 níveis — só pra exibição (card de revisão),
ver `portal_api.indicadores.calculo.composicao_individual`."""
composicao = indicadores_calculo.composicao_individual(obj)
return {
"individual": {
"percentual": float(composicao["individual"]["percentual"]),
"peso": float(composicao["individual"]["peso"]),
},
"grupo": {
"percentual": float(composicao["grupo"]["percentual"]),
"peso": float(composicao["grupo"]["peso"]),
},
"departamento": {
"percentual": float(composicao["departamento"]["percentual"]),
"peso": float(composicao["departamento"]["peso"]),
},
"total": float(composicao["total"]),
"ajustado_manualmente": composicao["ajustado_manualmente"],
}
class IndicadorApuracaoColaboradorAjusteSerializer(serializers.Serializer):
"""Ajuste manual do percentual Individual de um colaborador — passa a
valer no cálculo (`pct_individual_ajustado_manualmente=True`) até ser
revertido pela action `recalcular`. Grupo e Departamento não são ajustados
por colaborador (ver IndicadorApuracaoAjusteGrupoSerializer/
IndicadorApuracaoAjusteDepartamentoSerializer, em IndicadorApuracaoViewSet
— cada um deles vale de uma vez pra todo o gerente/apuração, ver CLAUDE.md)."""
pct_individual = serializers.DecimalField(max_digits=7, decimal_places=4)
class IndicadorApuracaoColaboradorValidadoSerializer(serializers.Serializer):
"""Checklist de revisão do RH (`IndicadorApuracaoColaborador.validado`) —
não afeta nenhum cálculo, ver `IndicadorApuracaoColaboradorViewSet.marcar_validado`."""
validado = serializers.BooleanField()
class IndicadorApuracaoAjusteGrupoSerializer(serializers.Serializer):
"""Ajuste manual do percentual Grupo — aplicado de uma vez a todos os
colaboradores do `gerente` informado dentro da apuração (ver
`IndicadorApuracaoViewSet.ajustar_grupo`)."""
gerente = serializers.CharField(allow_blank=True)
pct_grupo = serializers.DecimalField(max_digits=7, decimal_places=4)
class IndicadorApuracaoAjusteDepartamentoSerializer(serializers.Serializer):
"""Ajuste manual do percentual Departamento — aplicado de uma vez a todos
os colaboradores do `departamento` (id de um IndicadorDepartamento)
informado dentro da apuração (cada departamento tem sua própria meta de
Departamento, ver `IndicadorApuracaoViewSet.ajustar_departamento`)."""
departamento = serializers.IntegerField()
pct_departamento = serializers.DecimalField(max_digits=7, decimal_places=4)
class IndicadorApuracaoRecalcularGrupoSerializer(serializers.Serializer):
gerente = serializers.CharField(allow_blank=True)
class IndicadorApuracaoRecalcularDepartamentoSerializer(serializers.Serializer):
departamento = serializers.IntegerField()
class IndicadorDepartamentoGerenteSerializer(serializers.ModelSerializer):
departamento_nome = serializers.CharField(source="departamento.nome", read_only=True)
class Meta:
model = IndicadorDepartamentoGerente
fields = ["id", "departamento", "departamento_nome", "nome_gerente"]
read_only_fields = ["id"]
class IndicadorDepartamentoSerializer(serializers.ModelSerializer):
gerentes = serializers.SlugRelatedField(slug_field="nome_gerente", many=True, read_only=True)
class Meta:
model = IndicadorDepartamento
fields = ["id", "nome", "ativo", "gerentes", "criado_em"]
read_only_fields = ["id", "gerentes", "criado_em"]
class IndicadorApuracaoAjusteHonorarioEmpresaSerializer(serializers.Serializer):
"""Preenchimento manual do honorário de uma empresa **por código**, de uma
vez pra todos os colaboradores da apuração que a têm em
`honorario_nao_encontrado=True` — a mesma empresa pode aparecer em mais de
um colaborador (um por balancete, outro por liberação fiscal etc.), mas o
honorário é sempre o mesmo valor, então informar uma vez já resolve todo
mundo (ver `IndicadorApuracaoViewSet.ajustar_honorario_empresa`)."""
codigo_empresa = serializers.CharField()
honorario = serializers.DecimalField(max_digits=12, decimal_places=2)
class IndicadorApuracaoListSerializer(serializers.ModelSerializer):
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
total_colaboradores = serializers.SerializerMethodField()
class Meta:
model = IndicadorApuracao
fields = [
"id",
"competencia",
"status",
"criado_por_nome",
"criado_em",
"concluida_em",
"total_colaboradores",
]
def get_total_colaboradores(self, obj: IndicadorApuracao) -> int:
return obj.colaboradores.count()
class IndicadorApuracaoDetailSerializer(serializers.ModelSerializer):
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
colaboradores = IndicadorApuracaoColaboradorSerializer(many=True, read_only=True)
class Meta:
model = IndicadorApuracao
fields = [
"id",
"competencia",
"status",
"criado_por_nome",
"criado_em",
"concluida_em",
"avisos",
"colaboradores",
]
class SimulacaoCustoContratacaoEntradaSerializer(serializers.Serializer):
"""Entrada do formulário de "Simulação de Custo de Contratação" (Geradoc) —
sem model por trás, a ferramenta não persiste nada (ver CLAUDE.md). Campos
monetários/percentuais chegam como texto no formato brasileiro (ex.:
"8.500,00") e são convertidos com `parse_valor_br`, mesmo padrão já usado
em ImportacaoPlanoSaudeCreateSerializer."""
nome_referencia = serializers.CharField(required=False, allow_blank=True, max_length=200, default="")
empresa = serializers.CharField(required=False, allow_blank=True, max_length=200, default="")
cargo = serializers.CharField(required=False, allow_blank=True, max_length=200, default="")
salario_contratual = serializers.CharField()
adicional_art62 = serializers.CharField(required=False, allow_blank=True, default="0")
comissao = serializers.CharField(required=False, allow_blank=True, default="0")
dsr = serializers.CharField(required=False, allow_blank=True, default="0")
outros_itens_remuneracao = serializers.ListField(child=serializers.DictField(), required=False, default=list)
percentual_inss_empregador = serializers.CharField()
percentual_fgts = serializers.CharField()
dependentes = serializers.IntegerField(required=False, min_value=0, default=0)
vale_transporte = serializers.CharField(required=False, allow_blank=True, default="0")
seguro_vida = serializers.CharField(required=False, allow_blank=True, default="0")
vale_alimentacao = serializers.CharField(required=False, allow_blank=True, default="0")
assiduidade = serializers.CharField(required=False, allow_blank=True, default="0")
fundo_formacao = serializers.CharField(required=False, allow_blank=True, default="0")
outros_beneficios = serializers.ListField(child=serializers.DictField(), required=False, default=list)
desconto_vale_transporte = serializers.CharField(required=False, allow_blank=True, default="0")
desconto_vale_alimentacao = serializers.CharField(required=False, allow_blank=True, default="0")
desconto_sindicato = serializers.CharField(required=False, allow_blank=True, default="0")
outros_descontos = serializers.ListField(child=serializers.DictField(), required=False, default=list)
def _valida_itens_nome_valor(self, valor: Any, nome_campo: str) -> list[dict[str, str]]:
itens = []
for item in valor:
nome = str(item.get("nome", "")).strip()
if not nome:
raise serializers.ValidationError(f"Informe o nome de cada item em {nome_campo}.")
itens.append({"nome": nome, "valor": str(item.get("valor", "0"))})
return itens
def validate_outros_itens_remuneracao(self, valor: Any) -> list[dict[str, str]]:
return self._valida_itens_nome_valor(valor, "outros itens de remuneração")
def validate_outros_beneficios(self, valor: Any) -> list[dict[str, str]]:
return self._valida_itens_nome_valor(valor, "outros benefícios")
def validate_outros_descontos(self, valor: Any) -> list[dict[str, str]]:
return self._valida_itens_nome_valor(valor, "outros descontos")
def validate_salario_contratual(self, valor: str) -> str:
if parse_valor_br(valor) <= 0:
raise serializers.ValidationError("Informe o salário contratual.")
return valor
def validate_percentual_inss_empregador(self, valor: str) -> str:
if not (0 <= parse_valor_br(valor) <= 100):
raise serializers.ValidationError("Informe um percentual entre 0 e 100.")
return valor
def validate_percentual_fgts(self, valor: str) -> str:
if not (0 <= parse_valor_br(valor) <= 100):
raise serializers.ValidationError("Informe um percentual entre 0 e 100.")
return valor
def to_entrada(self) -> EntradaSimulacaoEmpregado:
dados = self.validated_data
return EntradaSimulacaoEmpregado(
nome_referencia=dados["nome_referencia"],
empresa=dados["empresa"],
cargo=dados["cargo"],
salario_contratual=parse_valor_br(dados["salario_contratual"]),
adicional_art62=parse_valor_br(dados["adicional_art62"]),
comissao=parse_valor_br(dados["comissao"]),
dsr=parse_valor_br(dados["dsr"]),
outros_itens_remuneracao=[
(item["nome"], parse_valor_br(item["valor"])) for item in dados["outros_itens_remuneracao"]
],
percentual_inss_empregador=parse_valor_br(dados["percentual_inss_empregador"]) / 100,
percentual_fgts=parse_valor_br(dados["percentual_fgts"]) / 100,
dependentes=dados["dependentes"],
vale_transporte=parse_valor_br(dados["vale_transporte"]),
seguro_vida=parse_valor_br(dados["seguro_vida"]),
vale_alimentacao=parse_valor_br(dados["vale_alimentacao"]),
assiduidade=parse_valor_br(dados["assiduidade"]),
fundo_formacao=parse_valor_br(dados["fundo_formacao"]),
outros_beneficios=[(item["nome"], parse_valor_br(item["valor"])) for item in dados["outros_beneficios"]],
desconto_vale_transporte=parse_valor_br(dados["desconto_vale_transporte"]),
desconto_vale_alimentacao=parse_valor_br(dados["desconto_vale_alimentacao"]),
desconto_sindicato=parse_valor_br(dados["desconto_sindicato"]),
outros_descontos=[(item["nome"], parse_valor_br(item["valor"])) for item in dados["outros_descontos"]],
)
def _valida_faixas(valor: Any, nome_campo: str) -> list[dict[str, float]]:
"""Valida o formato de `faixas_inss`/`faixas_irrf` (lista de
{limite_superior, aliquota, deduzir}) e devolve já ordenada por
`limite_superior` — a UI não precisa garantir a ordem."""
if not isinstance(valor, list) or not valor:
raise serializers.ValidationError(f"Informe ao menos uma faixa de {nome_campo}.")
faixas = []
for item in valor:
try:
limite = float(item["limite_superior"])
aliquota = float(item["aliquota"])
deduzir = float(item["deduzir"])
except (KeyError, TypeError, ValueError):
raise serializers.ValidationError(
f"Cada faixa de {nome_campo} precisa de limite_superior, aliquota e deduzir numéricos."
)
if limite <= 0:
raise serializers.ValidationError(f"O limite superior de uma faixa de {nome_campo} precisa ser positivo.")
if not (0 <= aliquota <= 1):
raise serializers.ValidationError(f"A alíquota de uma faixa de {nome_campo} precisa estar entre 0 e 1.")
faixas.append({"limite_superior": limite, "aliquota": aliquota, "deduzir": deduzir})
faixas.sort(key=lambda f: f["limite_superior"])
return faixas
class ParametroFiscalCustoContratacaoSerializer(serializers.ModelSerializer):
"""Tabelas de INSS/IRRF + parâmetros da Lei 15.270/2025 usados pela
Simulação de Custo de Contratação — editável pela própria tela, sem
histórico: é sempre "o valor vigente agora" que vale (ver CLAUDE.md)."""
class Meta:
model = ParametroFiscalCustoContratacao
fields = [
"faixas_inss",
"teto_desconto_inss",
"faixas_irrf",
"aliquota_irrf_topo",
"deduzir_irrf_topo",
"desconto_simplificado_irrf",
"deducao_por_dependente",
"reducao_lei_15270_coeficiente_a",
"reducao_lei_15270_coeficiente_b",
"reducao_lei_15270_limite",
"atualizado_em",
]
read_only_fields = ["atualizado_em"]
def validate_faixas_inss(self, valor: Any) -> list[dict[str, float]]:
return _valida_faixas(valor, "INSS")
def validate_faixas_irrf(self, valor: Any) -> list[dict[str, float]]:
return _valida_faixas(valor, "IRRF")