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 0000000..d5f96ed Binary files /dev/null and b/media/planos_saude/operadora/DEMONSTRATIVO_UNIMED_VkO8uGr.pdf differ 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 @@ + +