Inclusão da visualização das regras de importação do plano de saúde.
This commit is contained in:
parent
8942c2c4f8
commit
3541917a0b
@ -515,7 +515,7 @@ Pra adicionar uma operadora nova: criar `operadoras/<nome>/<arquivo>.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.
|
- `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".
|
- `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.
|
- `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
|
### 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.
|
- **`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` — `<input>` `#ips-regra-search` + lista flutuante `#ips-regra-combo-list`, filtra por nome a cada tecla; era um `<select>` simples, trocado quando o banco de regras cresceu o bastante pra não caber numa lista sem busca) + "Aplicar" preenche o formulário inteiro (tipos de lançamento + custeio de cada combinação + operadora, se ainda existir na lista) a partir de uma regra salva — os campos continuam 100% editáveis depois, é só um preenchimento em massa (`aplicarCusteio()`/`aplicarRegraNoFormulario()`), mesmo espírito do antigo "Importar regra". `regraSelecionadaId` (JS) rastreia o que está de fato escolhido no combobox — digitar de novo no campo invalida a seleção anterior até o usuário clicar numa regra da lista, pra "Aplicar" nunca usar uma regra desatualizada em relação ao texto exibido. **"Limpar seleção"** (`#ips-regra-limpar-btn`, ao lado de "Aplicar" — adicionado pro caso de aplicar a regra errada por engano) chama `limparRegraSelecionada()` (zera o rastreamento — combobox, `regraSelecionadaId`/`regraAplicadaId`, observações), `limparCusteioForm()` (desfaz o que a regra preencheu: desmarca tipos de lançamento, radios de custeio e campos de limite/percentual de cada combinação tipo×pessoa) **e** `limparOperadoraSelecionada()` (limpa também a Operadora, já que aplicar uma regra pode ter preenchido esse campo junto — ver combobox de Operadora abaixo) — as duas primeiras foram extraídas de dentro de `resetForm()` justamente pra serem reaproveitadas aqui, e as três juntas são exatamente o que `resetForm()` também chama; não mexe nos arquivos já anexados, só no que uma regra aplicada de fato preenche em massa. **"Ver regras salvas"** (`#ips-regras-modal`) continua no topo, junto de Aplicar/Limpar; **"Salvar regra atual..."** (`#ips-regra-salvar-btn`) foi movido pro **final do formulário** (depois de "Tipo de importação", antes do botão "Processar" — `.ips-regra-salvar-field` em `importacao-plano-saude.css`), decisão explícita do usuário: salvar só faz sentido depois de parametrizar o custeio, é o último passo do fluxo de criar/editar uma regra, não algo que deveria ficar ao lado de Aplicar/Ver regras salvas no topo. Continua abrindo o mesmo modal (`#ips-regra-save-modal`) pra nomear/descrever a configuração atualmente preenchida (validada antes com a mesma `mensagemErroCusteio()` usada pelo botão "Processar", reaproveitada pelas duas ações) — **editar uma regra existente é literalmente aplicá-la, ajustar o que quiser no formulário, e salvar de novo**: o modal nasce em modo "atualizar a regra selecionada" sempre que a regra atualmente refletida no formulário (`regraAplicadaId`) ainda existir, com uma checkbox pra optar por "criar uma nova regra" em vez de sobrescrever. "Ver regras salvas" lista todas as regras (nome, operadora, tipos, observações) com ações de Aplicar/Excluir — não duplica a grade de custeio num modal separado, de propósito, pra não manter dois lugares editáveis da mesma coisa.
|
- **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` — `<input>` `#ips-regra-search` + lista flutuante `#ips-regra-combo-list`, filtra por nome a cada tecla; era um `<select>` simples, trocado quando o banco de regras cresceu o bastante pra não caber numa lista sem busca) + "Aplicar" preenche o formulário inteiro (tipos de lançamento + custeio de cada combinação + operadora, se ainda existir na lista) a partir de uma regra salva — os campos continuam 100% editáveis depois, é só um preenchimento em massa (`aplicarCusteio()`/`aplicarRegraNoFormulario()`), mesmo espírito do antigo "Importar regra". `regraSelecionadaId` (JS) rastreia o que está de fato escolhido no combobox — digitar de novo no campo invalida a seleção anterior até o usuário clicar numa regra da lista, pra "Aplicar" nunca usar uma regra desatualizada em relação ao texto exibido. **"Limpar seleção"** (`#ips-regra-limpar-btn`, ao lado de "Aplicar" — adicionado pro caso de aplicar a regra errada por engano) chama `limparRegraSelecionada()` (zera o rastreamento — combobox, `regraSelecionadaId`/`regraAplicadaId`, observações), `limparCusteioForm()` (desfaz o que a regra preencheu: desmarca tipos de lançamento, radios de custeio e campos de limite/percentual de cada combinação tipo×pessoa) **e** `limparOperadoraSelecionada()` (limpa também a Operadora, já que aplicar uma regra pode ter preenchido esse campo junto — ver combobox de Operadora abaixo) — as duas primeiras foram extraídas de dentro de `resetForm()` justamente pra serem reaproveitadas aqui, e as três juntas são exatamente o que `resetForm()` também chama; não mexe nos arquivos já anexados, só no que uma regra aplicada de fato preenche em massa. **"Ver regras salvas"** (`#ips-regras-modal`) continua no topo, junto de Aplicar/Limpar; **"Salvar regra atual..."** (`#ips-regra-salvar-btn`) foi movido pro **final do formulário** (depois de "Tipo de importação", antes do botão "Processar" — `.ips-regra-salvar-field` em `importacao-plano-saude.css`), decisão explícita do usuário: salvar só faz sentido depois de parametrizar o custeio, é o último passo do fluxo de criar/editar uma regra, não algo que deveria ficar ao lado de Aplicar/Ver regras salvas no topo. Continua abrindo o mesmo modal (`#ips-regra-save-modal`) pra nomear/descrever a configuração atualmente preenchida (validada antes com a mesma `mensagemErroCusteio()` usada pelo botão "Processar", reaproveitada pelas duas ações) — **editar uma regra existente é literalmente aplicá-la, ajustar o que quiser no formulário, e salvar de novo**: o modal nasce em modo "atualizar a regra selecionada" sempre que a regra atualmente refletida no formulário (`regraAplicadaId`) ainda existir, com uma checkbox pra optar por "criar uma nova regra" em vez de sobrescrever. "Ver regras salvas" lista todas as regras (nome, operadora, tipos, observações) com ações de Aplicar/Excluir — não duplica a grade de custeio num modal separado, de propósito, pra não manter dois lugares editáveis da mesma coisa.
|
||||||
- **Classes `.ips-combo`/`.ips-combo__list`/`.ips-combo__item`/`.ips-combo__empty` (`importacao-plano-saude.css`) são genéricas**, não específicas de "Regra de custeio salva" — reaproveitadas também pelo campo "Operadora" (`#ips-operadora-combo`/`#ips-operadora-search`/`#ips-operadora-combo-list`), que virou o mesmo tipo de combobox pesquisável (era um `<select>` 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 `<input type="hidden" id="ips-form-operadora">` — 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 `<select>` disparava. Mesmo cuidado de invalidar a seleção ao digitar de novo, até escolher um item da lista. `limparOperadoraSelecionada()` (limpa o `<input type="hidden">` + 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.
|
- **Classes `.ips-combo`/`.ips-combo__list`/`.ips-combo__item`/`.ips-combo__empty` (`importacao-plano-saude.css`) são genéricas**, não específicas de "Regra de custeio salva" — reaproveitadas também pelo campo "Operadora" (`#ips-operadora-combo`/`#ips-operadora-search`/`#ips-operadora-combo-list`), que virou o mesmo tipo de combobox pesquisável (era um `<select>` 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 `<input type="hidden" id="ips-form-operadora">` — 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 `<select>` disparava. Mesmo cuidado de invalidar a seleção ao digitar de novo, até escolher um item da lista. `limparOperadoraSelecionada()` (limpa o `<input type="hidden">` + 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)
|
### 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.
|
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.
|
- **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 — `<nome>`". 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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
BIN
media/planos_saude/operadora/DEMONSTRATIVO_UNIMED_VkO8uGr.pdf
Normal file
BIN
media/planos_saude/operadora/DEMONSTRATIVO_UNIMED_VkO8uGr.pdf
Normal file
Binary file not shown.
1
media/planos_saude/operadora/arquivo.csv
Normal file
1
media/planos_saude/operadora/arquivo.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
x
|
||||||
|
@ -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;
|
||||||
|
1
media/planos_saude/planilha_padrao/planilha.csv
Normal file
1
media/planos_saude/planilha_padrao/planilha.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
x
|
||||||
|
14
plano.md
14
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).
|
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 — `<nome>`".
|
||||||
|
|
||||||
## Roadmap / próximos passos
|
## Roadmap / próximos passos
|
||||||
|
|
||||||
Nenhuma pendência explícita em aberto no momento, exceto a limitação conhecida
|
Nenhuma pendência explícita em aberto no momento, exceto a limitação conhecida
|
||||||
|
|||||||
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -547,6 +547,18 @@ class ImportacaoPlanoSaude(models.Model):
|
|||||||
# vazio nesse caso). Mutuamente exclusivo com o custeio manual de
|
# vazio nesse caso). Mutuamente exclusivo com o custeio manual de
|
||||||
# mensalidade na tela — ver ImportacaoPlanoSaudeCreateSerializer.
|
# mensalidade na tela — ver ImportacaoPlanoSaudeCreateSerializer.
|
||||||
regra_empresa = models.CharField("Regra empresa (mensalidade)", max_length=50, blank=True)
|
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_padrao = models.FileField(
|
||||||
"Planilha padrão (Questor)",
|
"Planilha padrão (Questor)",
|
||||||
upload_to="planos_saude/planilha_padrao/",
|
upload_to="planos_saude/planilha_padrao/",
|
||||||
|
|||||||
@ -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
|
# 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
|
# 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).
|
# 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] = {
|
REGRAS_EMPRESA: Dict[str, dict] = {
|
||||||
"unimed_1778_tecnomyl": {
|
"unimed_1778_tecnomyl": {
|
||||||
"label": "1778 - Unimed (Tecnomyl)",
|
"label": "1778 - Unimed (Tecnomyl)",
|
||||||
"codigo_empresa": "1778",
|
"codigo_empresa": "1778",
|
||||||
"operadora": "unimed_saude",
|
"operadora": "unimed_saude",
|
||||||
"aplica": _regra_unimed_1778_tecnomyl,
|
"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]]:
|
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(
|
def valida_regra_empresa(
|
||||||
|
|||||||
@ -491,6 +491,13 @@ class ImportacaoPlanoSaudeCreateSerializer(serializers.Serializer):
|
|||||||
# validate(): quando preenchida, substitui o custeio manual de
|
# validate(): quando preenchida, substitui o custeio manual de
|
||||||
# "mensalidade" (custeio_mensalidade_titular/dependente ficam ignorados).
|
# "mensalidade" (custeio_mensalidade_titular/dependente ficam ignorados).
|
||||||
regra_empresa = serializers.CharField(required=False, allow_blank=True)
|
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_titular = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
|
||||||
custeio_mensalidade_dependente = 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_titular = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
|
||||||
@ -711,6 +718,11 @@ class ImportacaoPlanoSaudeDetailSerializer(serializers.ModelSerializer):
|
|||||||
alteracoes = ImportacaoPlanoSaudeAlteracaoSerializer(many=True, read_only=True)
|
alteracoes = ImportacaoPlanoSaudeAlteracaoSerializer(many=True, read_only=True)
|
||||||
resumo_por_tipo = serializers.SerializerMethodField()
|
resumo_por_tipo = serializers.SerializerMethodField()
|
||||||
regra_empresa_label = 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:
|
class Meta:
|
||||||
model = ImportacaoPlanoSaude
|
model = ImportacaoPlanoSaude
|
||||||
@ -722,6 +734,10 @@ class ImportacaoPlanoSaudeDetailSerializer(serializers.ModelSerializer):
|
|||||||
"custeio_por_tipo",
|
"custeio_por_tipo",
|
||||||
"regra_empresa",
|
"regra_empresa",
|
||||||
"regra_empresa_label",
|
"regra_empresa_label",
|
||||||
|
"regra_empresa_observacoes",
|
||||||
|
"regra_custeio_salva",
|
||||||
|
"regra_custeio_salva_nome",
|
||||||
|
"regra_custeio_salva_observacoes",
|
||||||
"status",
|
"status",
|
||||||
"criado_por_nome",
|
"criado_por_nome",
|
||||||
"criado_em",
|
"criado_em",
|
||||||
@ -739,6 +755,14 @@ class ImportacaoPlanoSaudeDetailSerializer(serializers.ModelSerializer):
|
|||||||
regra = REGRAS_EMPRESA.get(obj.regra_empresa)
|
regra = REGRAS_EMPRESA.get(obj.regra_empresa)
|
||||||
return regra["label"] if regra else None
|
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]]:
|
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
|
"""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
|
lançado (VALOR ou VALOREMPRESA diferente de "0") vs. quantas ficaram em
|
||||||
|
|||||||
@ -829,6 +829,7 @@ class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
|
|||||||
tipos_lancamento=tipos,
|
tipos_lancamento=tipos,
|
||||||
custeio_por_tipo=custeio_por_tipo,
|
custeio_por_tipo=custeio_por_tipo,
|
||||||
regra_empresa=dados["regra_empresa"],
|
regra_empresa=dados["regra_empresa"],
|
||||||
|
regra_custeio_salva=dados.get("regra_custeio_salva"),
|
||||||
criado_por=request.user,
|
criado_por=request.user,
|
||||||
planilha_padrao=dados["planilha_padrao"],
|
planilha_padrao=dados["planilha_padrao"],
|
||||||
arquivo_operadora=dados["arquivo_operadora"],
|
arquivo_operadora=dados["arquivo_operadora"],
|
||||||
|
|||||||
@ -398,6 +398,34 @@
|
|||||||
color: var(--text-primary);
|
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
|
/* 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
|
dependente — este é o agrupamento visual dessas duas sub-listas de
|
||||||
radios dentro de .ips-custeio-radios. */
|
radios dentro de .ips-custeio-radios. */
|
||||||
|
|||||||
@ -257,6 +257,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
const formBackBtn = document.getElementById("ips-form-back-btn");
|
const formBackBtn = document.getElementById("ips-form-back-btn");
|
||||||
|
|
||||||
const reviewSubtitle = document.getElementById("ips-review-subtitle");
|
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 reviewError = document.getElementById("ips-review-error");
|
||||||
const reviewBackBtn = document.getElementById("ips-review-back-btn");
|
const reviewBackBtn = document.getElementById("ips-review-back-btn");
|
||||||
const gerarBtn = document.getElementById("ips-gerar-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}` : "";
|
const regraEmpresaSufixo = importacao.regra_empresa_label ? ` — Regra empresa: ${importacao.regra_empresa_label}` : "";
|
||||||
reviewSubtitle.textContent =
|
reviewSubtitle.textContent =
|
||||||
`${importacao.nome_operadora} — criada em ${new Date(importacao.criado_em).toLocaleString("pt-BR")}${regraEmpresaSufixo}`;
|
`${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 = "";
|
reviewError.textContent = "";
|
||||||
renderTabs();
|
renderTabs();
|
||||||
showView("review");
|
showView("review");
|
||||||
@ -1323,6 +1344,10 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
formTipoRegraEmpresa.addEventListener("change", () => {
|
formTipoRegraEmpresa.addEventListener("change", () => {
|
||||||
if (regraEmpresaBox) regraEmpresaBox.hidden = !formTipoRegraEmpresa.checked;
|
if (regraEmpresaBox) regraEmpresaBox.hidden = !formTipoRegraEmpresa.checked;
|
||||||
if (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;
|
formTipoMensalidade.checked = false;
|
||||||
custeioMensalidade.hidden = true;
|
custeioMensalidade.hidden = true;
|
||||||
custeioMensalidade.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false));
|
custeioMensalidade.querySelectorAll('input[type="radio"]').forEach((r) => (r.checked = false));
|
||||||
@ -1784,6 +1809,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||||||
formData.append("tipos_lancamento", tipos.join(","));
|
formData.append("tipos_lancamento", tipos.join(","));
|
||||||
if (formTipoRegraEmpresa.checked && regraEmpresaSelecionada) {
|
if (formTipoRegraEmpresa.checked && regraEmpresaSelecionada) {
|
||||||
formData.append("regra_empresa", regraEmpresaSelecionada.key);
|
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) => {
|
tipos.forEach((tipo) => {
|
||||||
// "Regra empresa" cobre mensalidade sozinha — sem titular/dependente
|
// "Regra empresa" cobre mensalidade sozinha — sem titular/dependente
|
||||||
|
|||||||
@ -498,6 +498,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p class="modal-error" id="ips-review-error"></p>
|
<p class="modal-error" id="ips-review-error"></p>
|
||||||
|
|
||||||
<div class="pa-tabs" id="ips-tabs"></div>
|
<div class="pa-tabs" id="ips-tabs"></div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user