From 3541917a0b4ea7afb493543c0e4848aeacdc5632 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Wed, 19 Aug 2026 15:09:01 -0300 Subject: [PATCH] =?UTF-8?q?Inclus=C3=A3o=20da=20visualiza=C3=A7=C3=A3o=20d?= =?UTF-8?q?as=20regras=20de=20importa=C3=A7=C3=A3o=20do=20plano=20de=20sa?= =?UTF-8?q?=C3=BAde.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 6 ++-- .../DEMONSTRATIVO_UNIMED_VkO8uGr.pdf | Bin 0 -> 13085 bytes media/planos_saude/operadora/arquivo.csv | 1 + ...ARANA_-_COOPERATIVA_DE_TRABALH_68eO989.CSV | 5 +++ .../planos_saude/planilha_padrao/planilha.csv | 1 + plano.md | 14 ++++++++ ...mportacaoplanosaude_regra_custeio_salva.py | 19 +++++++++++ portal_api/models.py | 12 +++++++ portal_api/planos_saude/regras_empresa.py | 17 +++++++++- portal_api/serializers.py | 24 ++++++++++++++ portal_api/views.py | 1 + static/css/importacao-plano-saude.css | 28 ++++++++++++++++ static/js/importacao-plano-saude.js | 30 ++++++++++++++++++ templates/importacao-plano-saude.html | 5 +++ 14 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 media/planos_saude/operadora/DEMONSTRATIVO_UNIMED_VkO8uGr.pdf create mode 100644 media/planos_saude/operadora/arquivo.csv create mode 100644 media/planos_saude/planilha_padrao/LEIAUTE_IMPORTACAO_DESPESAS_MEDICAS_EMP_1601_LABORATORIO_DE_ANALISES_CLINICAS_OSWALDO_CRUZ_DE_MEDIANEIRA_LTDA_OPER_4709_UNIMED_DO_OESTE_DO_PARANA_-_COOPERATIVA_DE_TRABALH_68eO989.CSV create mode 100644 media/planos_saude/planilha_padrao/planilha.csv create mode 100644 portal_api/migrations/0040_importacaoplanosaude_regra_custeio_salva.py diff --git a/CLAUDE.md b/CLAUDE.md index 78fabcf..03aa9bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -515,7 +515,7 @@ Pra adicionar uma operadora nova: criar `operadoras//.py` impleme - `ImportacaoPlanoSaudeLinha`: uma linha da planilha padrão já casada com o valor do mês (espelha `LinhaSistema` campo a campo) — **todos os campos são editáveis** na tela de revisão antes de gerar o CSV (decisão explícita do usuário, não só os valores). `valor`/`valor_empresa` ficam como `CharField` no mesmo formato string do pipeline (`"51,69"`/`"0"`), não `DecimalField`, pra manter fidelidade 1:1 com o CSV final sem risco de arredondamento. - `ImportacaoPlanoSaudeAuditoria`: espelha `ItemAuditoria` — os campos extraídos do arquivo da operadora (`motivo`/`nome`/`valor`/`detalhe`...) são read-only na tela; `resolvida`/`linha_vinculada` são a exceção, graváveis via a resolução manual (ver "Resolução manual de auditoria por nome" abaixo). `MOTIVOS_RESOLVIVEIS = ("NOME_DIVERGENTE", "NAO_CADASTRADO")` (atributo de classe) é a lista dos dois motivos "de leitura/grafia de nome" que aceitam esse fluxo — `VALOR_NEGATIVO`/`TIPO_INVALIDO` são outra categoria de problema (valor real negativo, tipo de despesa não mapeado) e não têm solução por "essa é a mesma pessoa". - `ImportacaoPlanoSaudeAlteracao`: log de cada edição de campo/inclusão/exclusão de linha feita manualmente na revisão — ver seção "Alterações" abaixo. -- `RegraCusteioPlanoSaude`: regra de custeio salva pra reaplicar em importações futuras (ex.: "092 - Unimed") — ver "Regras de custeio salvas" abaixo. Lista compartilhada (sem FK pra `ImportacaoPlanoSaude`), mesmo espírito de `LinkFerramenta`/`AcessoGeral`. +- `RegraCusteioPlanoSaude`: regra de custeio salva pra reaplicar em importações futuras (ex.: "092 - Unimed") — ver "Regras de custeio salvas" abaixo. Lista compartilhada, mesmo espírito de `LinkFerramenta`/`AcessoGeral` — o próprio model não tem FK pra nada; é `ImportacaoPlanoSaude.regra_custeio_salva` que aponta pra cá (opcional, `SET_NULL`), só como registro de qual regra (se alguma) foi aplicada pra preencher aquele formulário. ### Fluxo e endpoints @@ -568,13 +568,15 @@ Substituiu o antigo par de botões "Exportar regra"/"Importar regra" (baixava/li - **`limite_valor`/`percentual` sempre em texto BR na entrada, float|None persistido**: igual ao resto do módulo, esses dois campos chegam como string BR (`"150,00"`) tanto no create de uma importação quanto no banco de regras — mas uma regra salva, uma vez lida de volta pelo `GET`, já vem com esses valores como `float` (formato final persistido). Pra `RegraCusteioPlanoSaudeSerializer` aceitar os dois formatos sem corromper o valor (`"150.0"` seria lido errado como 15000 por `parse_valor_br`, que só entende separador de milhar `.`/decimal `,`), `_valor_custeio_para_texto_br()` normaliza um float de volta pra texto BR (`formata_valor_br`) antes de repassar pro parser — isso é o que permite reenviar uma regra sem edição (ex.: só mudando o nome) sem precisar reformatar nada no frontend. - **Frontend** (`importacao-plano-saude.js`, seção "Regra de custeio salva" — **primeiro** campo do formulário de Nova Importação, antes até de "Operadora": decisão explícita do usuário, já que aplicar uma regra já preenche a operadora junto, então escolher a regra é o primeiro passo natural do fluxo, não um apêndice no final): um combobox pesquisável (`#ips-regra-combo` — `` `#ips-regra-search` + lista flutuante `#ips-regra-combo-list`, filtra por nome a cada tecla; era um `` simples) pra permitir buscar pelo código/nome da operadora, já que `label` agora vem prefixado com o código de cadastro no Questor (ver `GET /operadoras/` acima). Diferença de implementação: como Operadora não tem um botão "Aplicar" separado (é o próprio campo do formulário, não uma configuração aplicada em massa), o valor de fato submetido viaja num `` — escolher um item da lista (`selecionarOperadora()`) já grava o valor na hora e revalida o arquivo da operadora já anexado (`validadorArquivo.revalidarSeAnexado()`), mesmo efeito que o antigo evento `change` do `` + o texto de busca) é chamada tanto por `resetForm()` quanto por "Limpar seleção" da regra — decisão explícita do usuário: como aplicar uma regra pode ter preenchido a Operadora junto, desfazer a seleção da regra também desfaz o que ela preencheu ali. +- **`ImportacaoPlanoSaude.regra_custeio_salva`** (FK opcional, `SET_NULL`, pra `RegraCusteioPlanoSaude`) registra qual regra (se alguma) estava aplicada no formulário no momento do "Processar" — só informativo, não influencia `custeio_por_tipo` (que já é o que de fato vale pro processamento) nem o processamento em si. Preenchido no submit com `regraAplicadaId` (JS) quando "Regra empresa" **não** está marcada (as duas rastreiam coisas diferentes e nunca são enviadas juntas — marcar "Regra empresa" já limpa `regraAplicadaId` via `limparRegraSelecionada()`, e aplicar uma regra de custeio salva já limpa "Regra empresa" via `limparRegraEmpresa()` dentro de `aplicarCusteio()`). É o que alimenta `regra_custeio_salva_nome`/`regra_custeio_salva_observacoes` na tela de Revisão (ver "Observações da regra, só-leitura na tela de Revisão" acima) — pedido do usuário depois de notar que a observação de uma regra de custeio salva (ex.: "092 - Unimed") não aparecia lá, só a de "Regra empresa". ### Regra empresa (custeio especial de mensalidade por família) Terceiro checkbox de "Tipo de importação" (ao lado de Mensalidade/Coparticipação) — cobre regras de custeio negociadas com uma empresa específica que não cabem no desenho normal "por tipo de lançamento × titular/dependente" (ver "Regras de custeio salvas" acima), tipicamente porque são calculadas por **família inteira** (titular + todos os dependentes somados), não por pessoa. **Sem relação nenhuma com `RegraCusteioPlanoSaude`** — decisão explícita do usuário: é um registro fixo no código (`portal_api/planos_saude/regras_empresa.py`), cadastrado pelo desenvolvedor quando o cliente repassa uma regra nova, nunca pela tela. - **Mutuamente exclusivo com "Mensalidade"**: as duas são formas alternativas de configurar o **mesmo** tipo de lançamento `"mensalidade"` — marcar "Regra empresa" desmarca e esconde os radios de Mensalidade (e vice-versa), tanto no frontend (`importacao-plano-saude.js`, handlers de `change` dos dois checkboxes + `aplicarCusteio()`/`limparRegraEmpresa()`) quanto implicitamente no backend (`ImportacaoPlanoSaudeCreateSerializer.validate()` grava `custeio_por_tipo["mensalidade"] = {}` quando `regra_empresa` vem preenchido, ignorando `custeio_mensalidade_titular`/`dependente`). O `tipo_lancamento` persistido na `ImportacaoPlanoSaudeLinha` continua sendo `"mensalidade"` de qualquer forma — o CSV gerado (`mensalidade.csv`) não muda de nome nem de formato, já que é isso que o Questor espera importar; "Regra empresa" é só uma forma alternativa de **calcular** o mesmo valor, não um tipo de lançamento novo. -- **Registro** (`portal_api/planos_saude/regras_empresa.py`, `REGRAS_EMPRESA: Dict[str, dict]`): cada entrada tem `label` (exibido no modal "Selecionar regra"), `codigo_empresa` (código da empresa na planilha padrão pra qual a regra foi negociada — trava contra aplicar a regra errada numa planilha de outra empresa, ver validação abaixo), `operadora` (só informativo) e `aplica` (a função que faz o cálculo). 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). +- **Registro** (`portal_api/planos_saude/regras_empresa.py`, `REGRAS_EMPRESA: Dict[str, dict]`): cada entrada tem `label` (exibido no modal "Selecionar regra"), `codigo_empresa` (código da empresa na planilha padrão pra qual a regra foi negociada — trava contra aplicar a regra errada numa planilha de outra empresa, ver validação abaixo), `operadora` (só informativo), `aplica` (a função que faz o cálculo) e `observacoes` (opcional, texto livre explicando a regra). 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). +- **Observações da regra, 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 — decisão explícita do usuário sobre onde posicionar): `ImportacaoPlanoSaudeDetailSerializer.regra_empresa_observacoes` (`SerializerMethodField`) resolve `REGRAS_EMPRESA[obj.regra_empresa]["observacoes"]` a cada carregamento da importação — não é um campo persistido na `ImportacaoPlanoSaude`, sempre reflete o texto atual do registro no código (se a observação do registro mudar depois, importações antigas passam a mostrar o texto novo também, já que não há snapshot). `abrirRevisao()` (`importacao-plano-saude.js`) só mostra o bloco (`hidden` por padrão) quando o campo vem preenchido — a maioria das regras pode não ter `observacoes` cadastrada. **Mesmo bloco reaproveitado pra "Regra de custeio salva"** (ver `ImportacaoPlanoSaude.regra_custeio_salva` e "Regras de custeio salvas" abaixo): se não há `regra_empresa_observacoes` mas a importação tem `regra_custeio_salva_observacoes` (a observação real da `RegraCusteioPlanoSaude` aplicada, via `CharField(source="regra_custeio_salva.observacoes", default=None)`), o mesmo bloco exibe essa observação, com o rótulo trocado pra "Observações da regra de custeio salva — ``". As duas fontes nunca vêm preenchidas ao mesmo tempo (aplicar uma regra de custeio salva sempre desliga "Regra empresa" e vice-versa, ver `aplicarCusteio()`/handler de `formTipoRegraEmpresa` acima). - **Primeira regra**: `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. - **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. - **Só funciona com casamento por nome** (`chave_casamento == "nome"`, ex.: Unimed) — a agregação por família depende do agrupamento que `matcher._casa_por_nome` já faz (por `numero_titular`); `_casa_por_cpf` não tem esse agrupamento e não foi estendida pra suportar (não havia necessidade ainda). `regras_empresa.valida_regra_empresa()` recusa explicitamente (`RegraEmpresaIncompativelError`, capturada à parte em `views.py` pra devolver a mensagem certa, não o erro genérico de "formato de arquivo") se a operadora escolhida não for compatível, e também recusa se a planilha padrão anexada não tiver nenhuma linha com o `codigo_empresa` esperado pela regra — trava contra aplicar a regra da Tecnomyl na planilha de outra empresa por engano. diff --git a/media/planos_saude/operadora/DEMONSTRATIVO_UNIMED_VkO8uGr.pdf b/media/planos_saude/operadora/DEMONSTRATIVO_UNIMED_VkO8uGr.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d5f96ed72730541faa9991a770954750f4b88e04 GIT binary patch literal 13085 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 literal 0 HcmV?d00001 diff --git a/media/planos_saude/operadora/arquivo.csv b/media/planos_saude/operadora/arquivo.csv new file mode 100644 index 0000000..c1b0730 --- /dev/null +++ b/media/planos_saude/operadora/arquivo.csv @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/media/planos_saude/planilha_padrao/LEIAUTE_IMPORTACAO_DESPESAS_MEDICAS_EMP_1601_LABORATORIO_DE_ANALISES_CLINICAS_OSWALDO_CRUZ_DE_MEDIANEIRA_LTDA_OPER_4709_UNIMED_DO_OESTE_DO_PARANA_-_COOPERATIVA_DE_TRABALH_68eO989.CSV b/media/planos_saude/planilha_padrao/LEIAUTE_IMPORTACAO_DESPESAS_MEDICAS_EMP_1601_LABORATORIO_DE_ANALISES_CLINICAS_OSWALDO_CRUZ_DE_MEDIANEIRA_LTDA_OPER_4709_UNIMED_DO_OESTE_DO_PARANA_-_COOPERATIVA_DE_TRABALH_68eO989.CSV new file mode 100644 index 0000000..9433bcb --- /dev/null +++ b/media/planos_saude/planilha_padrao/LEIAUTE_IMPORTACAO_DESPESAS_MEDICAS_EMP_1601_LABORATORIO_DE_ANALISES_CLINICAS_OSWALDO_CRUZ_DE_MEDIANEIRA_LTDA_OPER_4709_UNIMED_DO_OESTE_DO_PARANA_-_COOPERATIVA_DE_TRABALH_68eO989.CSV @@ -0,0 +1,5 @@ +CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO +1601;ALINE PATRICIA RAMOS;076.350.759-85;4709;01/07/2026;;;0;0; +1601;DANTE HENRIQUE MUELLER;477.250.019-72;4709;01/07/2026;;;0;0; +1601;DANTE HENRIQUE MUELLER;477.250.019-72;4709;01/07/2026;CAIO PEREIRA MUELLER;053.273.449-16;0;0; +1601;DANTE HENRIQUE MUELLER;477.250.019-72;4709;01/07/2026;MARLENE PEREIRA MUELLER;466.162.920-53;0;0; diff --git a/media/planos_saude/planilha_padrao/planilha.csv b/media/planos_saude/planilha_padrao/planilha.csv new file mode 100644 index 0000000..c1b0730 --- /dev/null +++ b/media/planos_saude/planilha_padrao/planilha.csv @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/plano.md b/plano.md index db250b6..28efe68 100644 --- a/plano.md +++ b/plano.md @@ -896,6 +896,20 @@ Corrigido: quando o item é de mensalidade e a importação tem `regra_empresa` Validado com um teste de integração real (dentro de uma transação revertida de propósito, nada commitado): simulou vincular primeiro o dependente (R$172,51, abaixo do teto sozinho) e depois o titular (R$162,69) da mesma família reportada pelo usuário (Rafael Cornelius/Viviani Busko Souza) — os dois ficaram 100% custeados pela empresa em cada etapa, batendo com o esperado (família de R$335,20 no total, bem abaixo do teto de R$661,61). +### 62. "Regra empresa" ganha campo de observações, exibido só-leitura na tela de Revisão + +Pedido do usuário: quando a importação usa uma regra empresa com observação cadastrada, mostrar essa observação na tela de Revisão — sem permitir edição, só pra conferência — entre o cabeçalho ("Revisão" + informações da execução) e as abas Mensalidade/Coparticipação/Auditoria/Alterações. + +`REGRAS_EMPRESA` (`regras_empresa.py`) ganhou o campo opcional `observacoes` (texto livre) — a regra `unimed_1778_tecnomyl` já nasceu com uma explicando o teto de R$661,61 e a prioridade dependentes→titular (rodada 60). Exposto via `ImportacaoPlanoSaudeDetailSerializer.regra_empresa_observacoes` (resolvido a cada carregamento a partir do registro no código, não persistido — se o texto do registro mudar depois, importações antigas refletem o texto novo também). Frontend: novo bloco `#ips-review-regra-empresa-obs` em `importacao-plano-saude.html`, populado por `abrirRevisao()` e escondido quando a importação não usa regra empresa ou a regra não tem observação cadastrada. + +### 63. "Regras de custeio salvas" também ganham observação exibida na Revisão + +Usuário testou uma importação (código 1601, "1601 - Unimed Oeste PR", regra de custeio salva com observação "Custeado integralmente pelo empregado e sócio...") e notou que a observação não aparecia na tela de Revisão — só a de "Regra empresa" (rodada 62) tinha esse tratamento. + +`ImportacaoPlanoSaude` ganhou `regra_custeio_salva` (FK opcional, `SET_NULL`, pra `RegraCusteioPlanoSaude`, migração `0040`) — só registro informativo de qual regra salva (se alguma) foi aplicada no formulário antes do "Processar", preenchido no submit com `regraAplicadaId` (JS) quando "Regra empresa" não está marcada. As duas nunca vêm preenchidas juntas: marcar "Regra empresa" já limpa o rastreamento da regra de custeio salva (`limparRegraSelecionada()`), e aplicar uma regra de custeio salva já desliga "Regra empresa" (`limparRegraEmpresa()` dentro de `aplicarCusteio()`) — esse segundo ponto já existia da rodada 59, só faltava o primeiro (adicionado agora). + +O mesmo bloco `#ips-review-regra-empresa-obs` (rodada 62) foi reaproveitado: se não há observação de regra empresa mas há `regra_custeio_salva_observacoes` (novo campo em `ImportacaoPlanoSaudeDetailSerializer`, lendo `RegraCusteioPlanoSaude.observacoes` de verdade via FK), o bloco mostra essa observação com o rótulo "Observações da regra de custeio salva — ``". + ## Roadmap / próximos passos Nenhuma pendência explícita em aberto no momento, exceto a limitação conhecida diff --git a/portal_api/migrations/0040_importacaoplanosaude_regra_custeio_salva.py b/portal_api/migrations/0040_importacaoplanosaude_regra_custeio_salva.py new file mode 100644 index 0000000..1979c78 --- /dev/null +++ b/portal_api/migrations/0040_importacaoplanosaude_regra_custeio_salva.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.7 on 2026-08-19 17:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('portal_api', '0039_importacaoplanosaude_regra_empresa'), + ] + + operations = [ + migrations.AddField( + model_name='importacaoplanosaude', + name='regra_custeio_salva', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='importacoes_plano_saude', to='portal_api.regracusteioplanosaude'), + ), + ] diff --git a/portal_api/models.py b/portal_api/models.py index 8bcbc01..94efc3a 100644 --- a/portal_api/models.py +++ b/portal_api/models.py @@ -547,6 +547,18 @@ class ImportacaoPlanoSaude(models.Model): # vazio nesse caso). Mutuamente exclusivo com o custeio manual de # mensalidade na tela — ver ImportacaoPlanoSaudeCreateSerializer. regra_empresa = models.CharField("Regra empresa (mensalidade)", max_length=50, blank=True) + # Só registro informativo de qual "Regra de custeio salva" (se alguma) foi + # usada pra preencher este formulário — não influencia o processamento + # (custeio_por_tipo já é o que vale), só permite mostrar a observação da + # regra na tela de Revisão. SET_NULL pra não impedir excluir a regra + # salva depois; nunca obrigatório, a maioria das importações não usa uma. + regra_custeio_salva = models.ForeignKey( + "RegraCusteioPlanoSaude", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="importacoes_plano_saude", + ) planilha_padrao = models.FileField( "Planilha padrão (Questor)", upload_to="planos_saude/planilha_padrao/", diff --git a/portal_api/planos_saude/regras_empresa.py b/portal_api/planos_saude/regras_empresa.py index 97466bd..292284f 100644 --- a/portal_api/planos_saude/regras_empresa.py +++ b/portal_api/planos_saude/regras_empresa.py @@ -80,18 +80,33 @@ def _regra_unimed_1778_tecnomyl(linhas_e_valores: List[Tuple[Any, float]]) -> No # e registrar aqui. `codigo_empresa` é o código da empresa na planilha padrão # pra qual a regra foi negociada — usado só pra travar contra aplicar a regra # errada numa planilha de outra empresa (ver `valida_regra_empresa` abaixo). +# `observacoes` é opcional (texto livre explicando a regra em português) — +# exibida só-leitura no topo da tela de revisão (ver "regra_empresa_observacoes" +# em ImportacaoPlanoSaudeDetailSerializer) pra o colaborador conferir a regra +# aplicada sem precisar abrir o código. REGRAS_EMPRESA: Dict[str, dict] = { "unimed_1778_tecnomyl": { "label": "1778 - Unimed (Tecnomyl)", "codigo_empresa": "1778", "operadora": "unimed_saude", "aplica": _regra_unimed_1778_tecnomyl, + "observacoes": ( + "A Tecnomyl oferece uma ajuda de custo de até R$ 661,61 por família " + "(titular + dependentes juntos, independente da quantidade de " + "dependentes). Os dependentes são custeados primeiro; o titular " + "absorve o valor residual do teto, e o excedente (se houver) é " + "descontado do empregado. Coparticipação segue o custeio normal " + "configurado nesta importação, sem relação com esta regra." + ), }, } def lista_regras_empresa() -> List[Dict[str, str]]: - return [{"key": chave, "label": dados["label"]} for chave, dados in REGRAS_EMPRESA.items()] + return [ + {"key": chave, "label": dados["label"], "observacoes": dados.get("observacoes", "")} + for chave, dados in REGRAS_EMPRESA.items() + ] def valida_regra_empresa( diff --git a/portal_api/serializers.py b/portal_api/serializers.py index 503107e..bc02a13 100644 --- a/portal_api/serializers.py +++ b/portal_api/serializers.py @@ -491,6 +491,13 @@ class ImportacaoPlanoSaudeCreateSerializer(serializers.Serializer): # validate(): quando preenchida, substitui o custeio manual de # "mensalidade" (custeio_mensalidade_titular/dependente ficam ignorados). regra_empresa = serializers.CharField(required=False, allow_blank=True) + # Só registro informativo de qual "Regra de custeio salva" (se alguma) foi + # aplicada no formulário — não influencia o processamento, só permite + # mostrar a observação da regra na tela de Revisão (ver + # ImportacaoPlanoSaudeDetailSerializer.regra_custeio_salva_observacoes). + regra_custeio_salva = serializers.PrimaryKeyRelatedField( + queryset=RegraCusteioPlanoSaude.objects.all(), required=False, allow_null=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) @@ -711,6 +718,11 @@ class ImportacaoPlanoSaudeDetailSerializer(serializers.ModelSerializer): alteracoes = ImportacaoPlanoSaudeAlteracaoSerializer(many=True, read_only=True) resumo_por_tipo = serializers.SerializerMethodField() regra_empresa_label = serializers.SerializerMethodField() + regra_empresa_observacoes = serializers.SerializerMethodField() + regra_custeio_salva_nome = serializers.CharField(source="regra_custeio_salva.nome", read_only=True, default=None) + regra_custeio_salva_observacoes = serializers.CharField( + source="regra_custeio_salva.observacoes", read_only=True, default=None + ) class Meta: model = ImportacaoPlanoSaude @@ -722,6 +734,10 @@ class ImportacaoPlanoSaudeDetailSerializer(serializers.ModelSerializer): "custeio_por_tipo", "regra_empresa", "regra_empresa_label", + "regra_empresa_observacoes", + "regra_custeio_salva", + "regra_custeio_salva_nome", + "regra_custeio_salva_observacoes", "status", "criado_por_nome", "criado_em", @@ -739,6 +755,14 @@ class ImportacaoPlanoSaudeDetailSerializer(serializers.ModelSerializer): regra = REGRAS_EMPRESA.get(obj.regra_empresa) return regra["label"] if regra else None + def get_regra_empresa_observacoes(self, obj: ImportacaoPlanoSaude) -> str | None: + """Observação da regra (texto livre cadastrado em REGRAS_EMPRESA) — + exibida só-leitura no topo da tela de revisão, pra o colaborador + conferir a regra aplicada sem precisar abrir o código.""" + regra = REGRAS_EMPRESA.get(obj.regra_empresa) + observacoes = (regra or {}).get("observacoes") + return observacoes or 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 diff --git a/portal_api/views.py b/portal_api/views.py index 3bb4071..e118ee4 100644 --- a/portal_api/views.py +++ b/portal_api/views.py @@ -829,6 +829,7 @@ class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet): tipos_lancamento=tipos, custeio_por_tipo=custeio_por_tipo, regra_empresa=dados["regra_empresa"], + regra_custeio_salva=dados.get("regra_custeio_salva"), criado_por=request.user, planilha_padrao=dados["planilha_padrao"], arquivo_operadora=dados["arquivo_operadora"], diff --git a/static/css/importacao-plano-saude.css b/static/css/importacao-plano-saude.css index 1ade397..10abc9d 100644 --- a/static/css/importacao-plano-saude.css +++ b/static/css/importacao-plano-saude.css @@ -398,6 +398,34 @@ 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 + Mensalidade/Coparticipação/Auditoria/Alterações. */ +.ips-regra-empresa-observacoes { + margin-bottom: var(--space-4); + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-sm); + background: var(--bg-canvas); + border: 1px solid var(--border-subtle); +} + +.ips-regra-empresa-observacoes__label { + margin: 0 0 var(--space-1); + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-muted); +} + +.ips-regra-empresa-observacoes__texto { + margin: 0; + font-size: 0.85rem; + color: var(--text-secondary); + white-space: pre-wrap; +} + /* Cada tipo de lançamento tem uma regra própria pra titular e outra pra dependente — este é o agrupamento visual dessas duas sub-listas de radios dentro de .ips-custeio-radios. */ diff --git a/static/js/importacao-plano-saude.js b/static/js/importacao-plano-saude.js index 53a305d..7f7e3a3 100644 --- a/static/js/importacao-plano-saude.js +++ b/static/js/importacao-plano-saude.js @@ -257,6 +257,9 @@ document.addEventListener("DOMContentLoaded", async () => { const formBackBtn = document.getElementById("ips-form-back-btn"); const reviewSubtitle = document.getElementById("ips-review-subtitle"); + const reviewRegraEmpresaObs = document.getElementById("ips-review-regra-empresa-obs"); + 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"); const reviewBackBtn = document.getElementById("ips-review-back-btn"); const gerarBtn = document.getElementById("ips-gerar-btn"); @@ -923,6 +926,24 @@ document.addEventListener("DOMContentLoaded", async () => { const regraEmpresaSufixo = importacao.regra_empresa_label ? ` — Regra empresa: ${importacao.regra_empresa_label}` : ""; reviewSubtitle.textContent = `${importacao.nome_operadora} — criada em ${new Date(importacao.criado_em).toLocaleString("pt-BR")}${regraEmpresaSufixo}`; + 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. + if (importacao.regra_empresa_observacoes) { + reviewRegraEmpresaObsLabel.textContent = "Observações da regra empresa"; + reviewRegraEmpresaObsTexto.textContent = importacao.regra_empresa_observacoes; + reviewRegraEmpresaObs.hidden = false; + } 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; + } else { + reviewRegraEmpresaObs.hidden = true; + reviewRegraEmpresaObsTexto.textContent = ""; + } + } reviewError.textContent = ""; renderTabs(); showView("review"); @@ -1323,6 +1344,10 @@ document.addEventListener("DOMContentLoaded", async () => { formTipoRegraEmpresa.addEventListener("change", () => { if (regraEmpresaBox) regraEmpresaBox.hidden = !formTipoRegraEmpresa.checked; if (formTipoRegraEmpresa.checked) { + // Mesma exclusividade mútua do outro lado (formTipoMensalidade acima) + // — também limpa o rastreamento de "regra de custeio salva" aplicada, + // senão o submit enviaria os dois ao mesmo tempo pro backend. + limparRegraSelecionada(); formTipoMensalidade.checked = false; custeioMensalidade.hidden = true; custeioMensalidade.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false)); @@ -1784,6 +1809,11 @@ document.addEventListener("DOMContentLoaded", async () => { formData.append("tipos_lancamento", tipos.join(",")); if (formTipoRegraEmpresa.checked && regraEmpresaSelecionada) { formData.append("regra_empresa", regraEmpresaSelecionada.key); + } else if (regraAplicadaId) { + // Só registro informativo (ver ImportacaoPlanoSaude.regra_custeio_salva) + // — não influencia o processamento, só permite mostrar a observação + // da regra de custeio salva aplicada na tela de Revisão. + formData.append("regra_custeio_salva", regraAplicadaId); } tipos.forEach((tipo) => { // "Regra empresa" cobre mensalidade sozinha — sem titular/dependente diff --git a/templates/importacao-plano-saude.html b/templates/importacao-plano-saude.html index 04fafef..41236d5 100644 --- a/templates/importacao-plano-saude.html +++ b/templates/importacao-plano-saude.html @@ -498,6 +498,11 @@ + +