Inclusão das regas de custei na tela de revisão
This commit is contained in:
parent
444d76a159
commit
ca9f53837c
@ -70,7 +70,8 @@
|
||||
"Bash(curl -s http://127.0.0.1:8000/static/css/layout.css)",
|
||||
"Bash(curl -s http://127.0.0.1:8000/static/css/halloween-cobweb.css)",
|
||||
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" _scratch_insert_seasonal_toggle.py)",
|
||||
"Bash(rm _scratch_insert_seasonal_toggle.py)"
|
||||
"Bash(rm _scratch_insert_seasonal_toggle.py)",
|
||||
"Bash(sort -t' ' -k2 -n -u)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,3 +335,12 @@ Pedido do usuário (2026-09-15): o nome do arquivo baixado em "Gerar Arquivo" er
|
||||
- **`ImportacaoPlanoSaudeViewSet.gerar()`**: o `.zip` (2 tipos) passou a se chamar `"<nome_base>.zip"` em vez de `"importacao_plano_saude_<id>.zip"`; o `.csv` único (1 tipo) passou a se chamar `"<nome_base> - <tipo>.csv"` (ex.: `"1970 - 5060 - mensalidade.csv"`) em vez de só `"<tipo>.csv"` — mesma ambiguidade existia aí também (duas execuções da mesma empresa com operadoras diferentes e só 1 tipo cada geravam dois `mensalidade.csv` indistinguíveis). Os nomes dos arquivos **dentro** do `.zip` (`mensalidade.csv`/`coparticipacao.csv`) não mudaram, já estão desambiguados pelo nome do próprio zip.
|
||||
- **`ImportacaoPlanoSaudeDePaulaViewSet.gerar()`** recebeu o mesmo ajuste (`prefixo_fallback="importacao_plano_saude_de_paula"`, preservando o fallback distinto de antes) — os dois `gerar()` compartilham exatamente o mesmo código de geração de arquivo, então ficaria inconsistente resolver só um dos dois.
|
||||
- Sem migração, sem mudança de contrato de API — só o nome do arquivo no header `Content-Disposition` da resposta binária.
|
||||
|
||||
### Rodada 108 — Resumo da regra de custeio na tela de Revisão (tipos cobertos + mensalidade/coparticipação)
|
||||
|
||||
Pedido do usuário (2026-09-16), com print da tela de Revisão mostrando um espaço vazio abaixo do código/nome da operadora: até então, esse espaço só mostrava a **observação** da regra empresa/regra de custeio salva (`regra_empresa_observacoes`/`regra_custeio_salva_observacoes`, já existentes no `ImportacaoPlanoSaudeDetailSerializer` — nenhum campo novo no backend foi necessário) — sem o resumo de tipos cobertos e do próprio custeio por titular/dependente que "Nova Importação" já mostra (`#ips-imp-resumo`, ao resolver uma regra salva). O usuário queria os dois juntos na Revisão, "assim o usuário tem certeza que está conferindo com base na regra do cliente".
|
||||
|
||||
- **`#ips-review-regra-empresa-obs`** (`importacao-plano-saude.html`) ganhou três `<p>` novos antes do label/texto de observação (`#ips-review-resumo-tipos`/`-mensalidade`/`-coparticipacao`, reaproveitando as classes `.ips-imp-resumo__tipos`/`.ips-imp-resumo__linha` já existentes) — mesmo bloco visual de sempre, só que agora mostra o resumo sempre (quando a importação tem algum `tipos_lancamento`, o que é sempre o caso) e a observação só quando existir uma. `.ips-regra-empresa-observacoes` (CSS) virou `display:flex; flex-direction:column; gap:var(--space-2)` pra espaçar as linhas novas do mesmo jeito que `.ips-imp-resumo` já fazia.
|
||||
- **`renderReviewResumoCusteio(importacao)`** (`importacao-plano-saude.js`, nova) monta o resumo a partir dos campos já presentes na importação (`tipos_lancamento`/`custeio_por_tipo`/`regra_empresa`/`regra_empresa_label`) — **não** reaproveita `renderResumoRegra()`/`regrasEmpresaCache` de "Nova Importação" porque essa cache só é buscada ao abrir os modais de "Nova Importação"/"Cadastro de Regras", ficando vazia quando a Revisão é aberta direto do histórico. Em vez disso, `reviewResumoTextoTipo()` detecta cobertura por "regra especial da empresa" olhando se `custeio_por_tipo[tipo]` veio vazio (sem `titular`/`dependente`) com `regra_empresa` preenchido — o mesmo sinal que `ImportacaoPlanoSaudeCreateSerializer.validate()` já grava pra tipos cobertos por uma regra empresa (ver "zeram custeio_por_tipo[tipo] só pros tipos em regra_empresa_tipos" no `CLAUDE.md` deste pacote) — evitando uma segunda fonte de verdade sobre quais tipos a regra cobre.
|
||||
- `abrirRevisao()` chama essa função e decide se o bloco inteiro fica visível (`temResumo || temObs`) — antes só aparecia com observação cadastrada, agora aparece por padrão em toda importação com tipo de lançamento selecionado.
|
||||
- Sem migração, sem endpoint novo — só leitura de campos que a Revisão já carregava.
|
||||
|
||||
@ -225,7 +225,7 @@ Cobre regras de custeio negociadas com uma empresa específica que não cabem no
|
||||
- No backend, `RegraCusteioPlanoSaudeSerializer.validate()`/`ImportacaoPlanoSaudeCreateSerializer.validate()` calculam `regra_empresa_tipos` (interseção entre `REGRAS_EMPRESA[chave]["tipos_lancamento"]` e os tipos selecionados — erro claro se vier vazia), conferem `chave_casamento_para_tipo(tipo) == REGRAS_EMPRESA[chave]["chave_casamento"]` pra cada tipo coberto (não mais um "exige nome" hardcoded) e zeram `custeio_por_tipo[tipo]` só pros tipos em `regra_empresa_tipos` (os demais tipos selecionados continuam com custeio manual normal). `regras_empresa.valida_regra_empresa(regra_empresa_key, chave_casamento_por_tipo, linhas_sistema, tipos_selecionados)` devolve `(aplica, tipos_cobertos)` — `pipeline.processa_importacao` passa `regra_empresa_fn` pra `casa_individuos_com_planilha` só quando `tipo_lancamento in tipos_cobertos`.
|
||||
- **`matcher._casa_por_cpf` passou a suportar `regra_empresa_fn`** (antes só `_casa_por_nome` suportava) — sem agrupar por família (essa estratégia não tem esse conceito): acumula `(linha, valor_total)` de todo indivíduo casado por CPF e chama `regra_empresa_fn(linhas_e_valores, tipo_lancamento)` uma vez só, no fim, com todos os pares do tipo de lançamento inteiro. `aplica(linhas_e_valores, tipo_lancamento)` é a assinatura de toda regra agora (segundo argumento novo) — permite uma mesma função se comportar diferente por tipo (ver Ottimizza abaixo); a Tecnomyl recebe o parâmetro mas ignora (só é chamada pra "mensalidade" mesmo, via `tipos_lancamento`).
|
||||
- **Registro** (`REGRAS_EMPRESA`): cada entrada tem `label`, `codigo_empresa` (código da empresa na planilha padrão pra qual a regra foi negociada), `operadora`, `chave_casamento`, `tipos_lancamento`, `aplica` (função que faz o cálculo) e `observacoes`. Pra cadastrar uma regra nova: escrever a função e registrar aqui — nada mais precisa mudar (`GET /api/importacoes-plano-saude/regras-empresa/` já reflete o registro, incluindo `tipos_lancamento`, consumido tanto pelo seletor dentro do Cadastro de Regras quanto pelo resumo só-leitura de "Nova Importação").
|
||||
- **Observações da regra, só-leitura na tela de Revisão** (`#ips-review-regra-empresa-obs`) — inalterado: `ImportacaoPlanoSaudeDetailSerializer.regra_empresa_observacoes` resolve `REGRAS_EMPRESA[obj.regra_empresa]["observacoes"]` a cada carregamento; o mesmo bloco cai pra `regra_custeio_salva_observacoes` quando não há regra empresa.
|
||||
- **Resumo de custeio + observações, só-leitura na tela de Revisão** (`#ips-review-regra-empresa-obs`, entre o cabeçalho "Revisão" e as abas Mensalidade/Coparticipação/Auditoria/Alterações — pedido explícito do usuário, rodada 108, "assim o usuário tem certeza que está conferindo com base na regra do cliente"): mostra sempre "Tipos cobertos" + uma linha por tipo de lançamento (`Mensalidade`/`Coparticipação` — "Titular: .../Dependente: ..." ou "usa regra especial da empresa — <label>", mesmo texto de `renderResumoRegra()` de "Nova Importação", calculado à parte em `renderReviewResumoCusteio()`/`reviewResumoTextoTipo()` porque a Revisão não tem `regrasEmpresaCache` carregado quando aberta direto do histórico — cobertura por regra especial é detectada por `custeio_por_tipo[tipo]` vir vazio com `regra_empresa` preenchido, não por consulta a essa cache), seguido da observação quando existir uma. `ImportacaoPlanoSaudeDetailSerializer.regra_empresa_observacoes` resolve `REGRAS_EMPRESA[obj.regra_empresa]["observacoes"]` a cada carregamento; o mesmo bloco cai pra `regra_custeio_salva_observacoes` quando não há regra empresa.
|
||||
- **`unimed_1778_tecnomyl`** — Tecnomyl (código 1778 na Unimed) tem ajuda de custo de até R$ 661,61 por família (titular + dependentes juntos, não por pessoa): família com mensalidade total acima do teto tem o excedente descontado do empregado; igual ou abaixo do teto, a empresa cobre 100%. Repassada pelo cliente em 08/2026. `chave_casamento="nome"` (precisa agrupar família), `tipos_lancamento=("mensalidade",)` — coparticipação dela segue sempre o custeio normal configurado no mesmo cadastro (radios titular/dependente), sem nenhuma ligação com a regra.
|
||||
- **Cálculo é por família, com prioridade explícita: dependentes primeiro, titular absorve o residual** (`regras_empresa._aplica_teto_familia`) — decisão explícita do cliente, e diferente de uma primeira versão (revertida) que distribuía o teto **proporcionalmente** entre todas as linhas. O algoritmo percorre primeiro os dependentes (na ordem em que aparecem no arquivo da operadora), cada um recebendo `valor_empresa = min(seu valor, o que sobrou do teto)`; só depois de todos os dependentes processados o titular absorve o que sobrou do teto (`teto_restante`), com o excedente (se houver) virando desconto do empregado nessa mesma linha. Ex.: família com dependente de R$559,57 e titular de R$314,12 (teto R$661,61) — dependente sai com `valor_empresa=559,57`/`valor=0` (coberto integralmente), sobra `661,61-559,57=102,04` de teto pro titular, que sai com `valor_empresa=102,04`/`valor=212,08`. Se os dependentes sozinhos já consumirem o teto inteiro, o titular fica com `valor_empresa=0` (desconto integral) e, se ainda sobrar dependente sem cobrir depois disso, esse dependente também é parcialmente descontado. Validado rodando o pipeline direto com os dois exemplos passados pelo cliente (família de R$800 → R$661,61 empresa/R$138,39 empregado no total; família abaixo do teto → 100% empresa) e reproduzindo exatamente um caso real reportado pelo usuário (família Caroline Fernandes/Luciano Ramos, R$873,69 no total) depois do ajuste de prioridade.
|
||||
- **`_aplica_teto_familia` é duck-typed de propósito** (`linhas_e_valores: List[Tuple[Any, float]]`, `_eh_linha_titular()` própria em vez de `LinhaSistema.eh_linha_titular()`): roda tanto contra `LinhaSistema` (pipeline, na criação da importação) quanto contra `ImportacaoPlanoSaudeLinha` (model Django, no recálculo pós "Vincular pessoa" — ver `views._recalcula_familia_regra_empresa` e "Resolução manual de auditoria por nome" acima) — as duas classes têm os mesmos atributos de string (`nome_dependente`/`cpf_dependente`/`valor_empresa`/`valor`), só a segunda não tem o método `eh_linha_titular()`.
|
||||
|
||||
@ -586,9 +586,10 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Observações da regra empresa na tela de Revisão — só leitura, mesma
|
||||
linguagem visual de .ips-regra-observacoes (formulário de nova
|
||||
importação), fica entre o cabeçalho "Revisão" e as abas
|
||||
/* Resumo da regra aplicada + observações na tela de Revisão — só leitura,
|
||||
mesma linguagem visual de .ips-imp-resumo (formulário de nova
|
||||
importação, cujas classes __tipos/__linha são reaproveitadas aqui dentro),
|
||||
fica entre o cabeçalho "Revisão" e as abas
|
||||
Mensalidade/Coparticipação/Auditoria/Alterações. */
|
||||
.ips-regra-empresa-observacoes {
|
||||
margin-bottom: var(--space-4);
|
||||
@ -596,10 +597,13 @@
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-canvas);
|
||||
border: 1px solid var(--border-subtle);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.ips-regra-empresa-observacoes__label {
|
||||
margin: 0 0 var(--space-1);
|
||||
margin: 0;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
|
||||
@ -360,6 +360,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
const reviewEmpresa = document.getElementById("ips-review-empresa");
|
||||
const reviewSubtitle = document.getElementById("ips-review-subtitle");
|
||||
const reviewRegraEmpresaObs = document.getElementById("ips-review-regra-empresa-obs");
|
||||
const reviewResumoTipos = document.getElementById("ips-review-resumo-tipos");
|
||||
const reviewResumoMensalidade = document.getElementById("ips-review-resumo-mensalidade");
|
||||
const reviewResumoCoparticipacao = document.getElementById("ips-review-resumo-coparticipacao");
|
||||
const reviewRegraEmpresaObsLabel = document.getElementById("ips-review-regra-empresa-obs-label");
|
||||
const reviewRegraEmpresaObsTexto = document.getElementById("ips-review-regra-empresa-obs-texto");
|
||||
const reviewError = document.getElementById("ips-review-error");
|
||||
@ -1340,6 +1343,68 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Mesmo texto de linha usado em renderResumoRegra() acima, mas pra tela
|
||||
// de Revisão — que não parte de uma "regra" em memória, e sim dos campos
|
||||
// já persistidos na própria importação. Cobertura por regra especial da
|
||||
// empresa é detectada pelo custeio_por_tipo[tipo] vir vazio (sem
|
||||
// titular/dependente) quando `regraEmpresaChave` está presente — mesmo
|
||||
// sinal que ImportacaoPlanoSaudeCreateSerializer.validate() grava (ver
|
||||
// "zeram custeio_por_tipo[tipo] só pros tipos em regra_empresa_tipos" no
|
||||
// CLAUDE.md deste pacote) — evita depender de regrasEmpresaCache, que não
|
||||
// é carregado quando a Revisão é aberta direto do histórico.
|
||||
function reviewResumoTextoTipo(tipoLabel, tipo, custeioPorTipo, regraEmpresaChave, regraEmpresaLabel) {
|
||||
const entrada = custeioPorTipo[tipo] || {};
|
||||
if (regraEmpresaChave && !entrada.titular && !entrada.dependente) {
|
||||
return `${tipoLabel}: usa regra especial da empresa — ${regraEmpresaLabel || regraEmpresaChave}`;
|
||||
}
|
||||
const titular = resumoModoPessoa(entrada.titular);
|
||||
const dependente = resumoModoPessoa(entrada.dependente);
|
||||
return `${tipoLabel} — Titular: ${titular}. Dependente: ${dependente}.`;
|
||||
}
|
||||
|
||||
// Preenche o resumo de custeio (tipos cobertos + mensalidade/
|
||||
// coparticipação) dentro de #ips-review-regra-empresa-obs — devolve se
|
||||
// havia algo pra mostrar, pra abrirRevisao() decidir se o bloco inteiro
|
||||
// (resumo + observação) precisa ficar visível.
|
||||
function renderReviewResumoCusteio(importacao) {
|
||||
const tipos = importacao.tipos_lancamento || [];
|
||||
const temResumo = tipos.length > 0;
|
||||
if (reviewResumoTipos) {
|
||||
reviewResumoTipos.textContent = `Tipos cobertos: ${tiposLabel(tipos)}`;
|
||||
reviewResumoTipos.hidden = !temResumo;
|
||||
}
|
||||
const custeioPorTipo = importacao.custeio_por_tipo || {};
|
||||
if (reviewResumoMensalidade) {
|
||||
if (tipos.includes("mensalidade")) {
|
||||
reviewResumoMensalidade.textContent = reviewResumoTextoTipo(
|
||||
"Mensalidade",
|
||||
"mensalidade",
|
||||
custeioPorTipo,
|
||||
importacao.regra_empresa,
|
||||
importacao.regra_empresa_label
|
||||
);
|
||||
reviewResumoMensalidade.hidden = false;
|
||||
} else {
|
||||
reviewResumoMensalidade.hidden = true;
|
||||
}
|
||||
}
|
||||
if (reviewResumoCoparticipacao) {
|
||||
if (tipos.includes("coparticipacao")) {
|
||||
reviewResumoCoparticipacao.textContent = reviewResumoTextoTipo(
|
||||
"Coparticipação",
|
||||
"coparticipacao",
|
||||
custeioPorTipo,
|
||||
importacao.regra_empresa,
|
||||
importacao.regra_empresa_label
|
||||
);
|
||||
reviewResumoCoparticipacao.hidden = false;
|
||||
} else {
|
||||
reviewResumoCoparticipacao.hidden = true;
|
||||
}
|
||||
}
|
||||
return temResumo;
|
||||
}
|
||||
|
||||
// Monta o FormData de "Nova Importação" a partir da regra resolvida —
|
||||
// mesmo formato de campos que ImportacaoPlanoSaudeCreateSerializer já
|
||||
// espera (custeio_<tipo>_<pessoa>, limite_valor_.../percentual_...), só
|
||||
@ -1720,22 +1785,33 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
reviewSubtitle.textContent =
|
||||
`${importacao.nome_operadora} — criada em ${new Date(importacao.criado_em).toLocaleString("pt-BR")}${regraEmpresaSufixo}${competenciaSufixo}`;
|
||||
if (reviewRegraEmpresaObs) {
|
||||
// As duas regras (empresa e de custeio salva) são mutuamente exclusivas
|
||||
// na criação — nunca as duas preenchidas ao mesmo tempo — mas o bloco é
|
||||
// o mesmo pra qualquer uma que tenha observação cadastrada.
|
||||
// Resumo (tipos cobertos + mensalidade/coparticipação) sempre que a
|
||||
// importação tiver algum tipo de lançamento (praticamente sempre) —
|
||||
// pedido explícito do usuário, pra conferir a regra do cliente sem
|
||||
// precisar abrir o Cadastro de Regras. As duas observações (regra
|
||||
// empresa e de custeio salva) são mutuamente exclusivas na criação —
|
||||
// nunca as duas preenchidas ao mesmo tempo.
|
||||
const temResumo = renderReviewResumoCusteio(importacao);
|
||||
let temObs = false;
|
||||
if (importacao.regra_empresa_observacoes) {
|
||||
reviewRegraEmpresaObsLabel.textContent = "Observações da regra empresa";
|
||||
reviewRegraEmpresaObsTexto.textContent = importacao.regra_empresa_observacoes;
|
||||
reviewRegraEmpresaObs.hidden = false;
|
||||
reviewRegraEmpresaObsLabel.hidden = false;
|
||||
reviewRegraEmpresaObsTexto.hidden = false;
|
||||
temObs = true;
|
||||
} else if (importacao.regra_custeio_salva_observacoes) {
|
||||
const nome = importacao.regra_custeio_salva_nome ? ` — ${importacao.regra_custeio_salva_nome}` : "";
|
||||
reviewRegraEmpresaObsLabel.textContent = `Observações da regra de custeio salva${nome}`;
|
||||
reviewRegraEmpresaObsTexto.textContent = importacao.regra_custeio_salva_observacoes;
|
||||
reviewRegraEmpresaObs.hidden = false;
|
||||
reviewRegraEmpresaObsLabel.hidden = false;
|
||||
reviewRegraEmpresaObsTexto.hidden = false;
|
||||
temObs = true;
|
||||
} else {
|
||||
reviewRegraEmpresaObs.hidden = true;
|
||||
reviewRegraEmpresaObsLabel.hidden = true;
|
||||
reviewRegraEmpresaObsTexto.hidden = true;
|
||||
reviewRegraEmpresaObsTexto.textContent = "";
|
||||
}
|
||||
reviewRegraEmpresaObs.hidden = !(temResumo || temObs);
|
||||
}
|
||||
reviewError.textContent = "";
|
||||
renderTabs();
|
||||
|
||||
@ -500,8 +500,11 @@
|
||||
</div>
|
||||
|
||||
<div class="ips-regra-empresa-observacoes" id="ips-review-regra-empresa-obs" hidden>
|
||||
<p class="ips-regra-empresa-observacoes__label" id="ips-review-regra-empresa-obs-label"></p>
|
||||
<p class="ips-regra-empresa-observacoes__texto" id="ips-review-regra-empresa-obs-texto"></p>
|
||||
<p class="ips-imp-resumo__tipos" id="ips-review-resumo-tipos"></p>
|
||||
<p class="ips-imp-resumo__linha" id="ips-review-resumo-mensalidade" hidden></p>
|
||||
<p class="ips-imp-resumo__linha" id="ips-review-resumo-coparticipacao" hidden></p>
|
||||
<p class="ips-regra-empresa-observacoes__label" id="ips-review-regra-empresa-obs-label" hidden></p>
|
||||
<p class="ips-regra-empresa-observacoes__texto" id="ips-review-regra-empresa-obs-texto" hidden></p>
|
||||
</div>
|
||||
|
||||
<p class="modal-error" id="ips-review-error"></p>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user