Revisão da tela de cadastro de regras - Plano de Saúde.

This commit is contained in:
Gabriel 2026-08-21 16:05:48 -03:00
parent 160459ddf6
commit 7fc771aafb
24 changed files with 1988 additions and 653 deletions

8
.claude/settings.json Normal file
View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(python -c ' *)",
"Bash(.venv/Scripts/python.exe manage.py shell -c ' *)"
]
}
}

View File

@ -148,7 +148,7 @@ Um único app, `portal_api/`:
| `/api/importacoes-plano-saude/{id}/gerar/` | POST | monta o CSV (ou ZIP, se mais de um tipo de lançamento) a partir das linhas já revisadas/editadas e devolve como download binário; marca a importação como `concluida` |
| `/api/importacoes-plano-saude-linhas/`, `/api/importacoes-plano-saude-linhas/{id}/` | GET/POST/PATCH/DELETE | edição/inclusão/exclusão de uma linha da revisão (todos os campos, não só valores); mesma permissão da importação, sem conceito de "dono"; as três operações também gravam um `ImportacaoPlanoSaudeAlteracao` (ver "Alterações" abaixo) |
| `/api/importacoes-plano-saude-alteracoes/{id}/reverter/` | POST | desfaz uma alteração específica (edição/inclusão/exclusão de linha) registrada na aba "Alterações" da revisão — ver seção própria abaixo |
| `/api/regras-custeio-plano-saude/`, `/api/regras-custeio-plano-saude/{id}/` | GET/POST/PATCH/DELETE | banco de regras de custeio salvas (`nome`+`operadora`+`tipos_lancamento`+`custeio_por_tipo`+`observacoes`, ver "Regras de custeio salvas" abaixo) — mesma permissão de toggle único da ferramenta; lista compartilhada, sem "dono" |
| `/api/regras-custeio-plano-saude/`, `/api/regras-custeio-plano-saude/{id}/` | GET/POST/PATCH/DELETE | banco de regras de custeio por empresa+operadora (`codigo_empresa`+`operadora`, únicos juntos+`regra_empresa_chave`+`tipos_lancamento`+`custeio_por_tipo`+`observacoes`; `nome` é sempre derivado, nunca aceito do cliente — ver "Cadastro de Regras" abaixo) — mesma permissão de toggle único da ferramenta; lista compartilhada, sem "dono"; cadastro/edição só pela tela "Cadastro de Regras", nunca em "Nova Importação" |
| `/api/simulacao-custo-contratacao/gerar/` | POST | calcula (`portal_api.custo_contratacao.calculo.calcula_custo_empregado`) e devolve o PDF direto na resposta (`application/pdf`, sem persistir nada); `PermissaoApp`-like check manual via `permissao_app("geradoc", "simulacao-custo-contratacao")` — ver seção própria abaixo |
| `/api/parametros-fiscais-custo-contratacao/` | GET/PATCH | tabelas de INSS/IRRF + parâmetros da Lei 15.270/2025 usados pela simulação (`ParametroFiscalCustoContratacao`, singleton `pk=1`); mesma permissão da simulação, sem par visualizar/editar dedicado |
| `/api/indicadores-percentuais-tipo/` | GET/POST/DELETE | histórico de percentuais individual/grupo/departamento por tipo de colaborador (`IndicadorPercentualTipo`) — nunca editado in-place, só criado com `vigente_desde` novo; mesma permissão de toggle único `apps["indicador-desempenho"]` em `permissoes["geradoc"]` |
@ -190,7 +190,7 @@ Um único app, `portal_api/`:
| `links-ferramentas.html` | Grade de cartões de atalho para ferramentas externas; reordenar/incluir/remover só com `apps["links-ferramentas-editar"]` (ver seção própria abaixo). |
| `acessos-gerais.html` | Cadastro de acessos/logins compartilhados organizados em seções e linhas, com popup de detalhes; criar/editar/excluir seções e linhas só com `apps["acessos-gerais-editar"]` (ver seção "Acessos Gerais" abaixo). |
| `ramais.html` | Diretório de ramais internos, filtros por nome/departamento; adicionar/editar ramal, criar ausência e excluir só com `apps.editar` em `ramais` (ver seção "Ramais" abaixo). |
| `importacao-plano-saude.html` | Ferramenta de Utilitários: histórico + nova importação (upload) + revisão/geração do arquivo de lançamento de plano de saúde (ver seção "Importação de Plano de Saúde" abaixo). |
| `importacao-plano-saude.html` | Ferramenta de Utilitários: histórico + cadastro de regras de custeio por empresa (separado da execução) + nova importação (upload, só aplica uma regra já cadastrada) + revisão/geração do arquivo de lançamento de plano de saúde (ver seção "Importação de Plano de Saúde" abaixo). |
| `custo-contratacao.html` | Ferramenta de Geradoc: formulário de simulação de custo de contratação (Empregado CLT) + painel colapsável de parâmetros fiscais, gera um PDF (ver seção "Simulação de Custo de Contratação" abaixo). |
| `indicador-desempenho.html` | Ferramenta de Geradoc: histórico de apurações + nova apuração (upload das 2 planilhas) + cadastro de critérios/percentuais + revisão/geração dos recibos em PDF do Indicador de Desempenho do Fiscontábil (ver seção própria abaixo). |
@ -535,13 +535,16 @@ portal_api/planos_saude/
├── itamed/saude.py Itamed Saúde (PDF via pdfplumber, mensalidade+coparticipação, casamento por nome)
├── dental_uni/odonto_mensalidade.py Dental Uni Odonto (PDF via pdfplumber, só mensalidade, casamento por nome)
├── unimed_oeste_pr/saude.py Unimed Oeste do Paraná (PDF via pdfplumber, mensalidade+coparticipação por texto da descrição, casamento por nome)
└── bradesco/saude.py Bradesco Saúde (PDF **sem texto selecionável** — OCR via `docling`, mensalidade+coparticipação, casamento por nome)
├── bradesco/saude.py Bradesco Saúde (PDF **sem texto selecionável** — OCR via `docling`, mensalidade+coparticipação, casamento por nome)
└── bradesco/odonto_mensalidade.py Bradesco Dental/Bradessaude Odonto — 3759 (PDF via pdfplumber, mensalidade+coparticipação, casamento por nome, ver nota abaixo)
```
Pra adicionar uma operadora nova: criar `operadoras/<nome>/<arquivo>.py` implementando `OperadoraParser.extrai()` (devolve `(List[Individuo], List[ItemAuditoria])`) e registrar em `pipeline.OPERADORAS`. **Antes de escrever o parser, ler `projects/importacao-planos-saude.skill`** — documenta decisões de negócio já validadas com o cliente (ex.: nome divergente nunca é resolvido por aproximação, valor final negativo vai pra auditoria, um mesmo beneficiário pode aparecer em várias linhas/rubricas e precisa ser somado) que não devem ser reinterpretadas sem confirmar de novo.
**PDF sem texto selecionável (ex.: Bradesco Saúde) precisa de OCR, não de `pdfplumber`**: confirmado rodando `pdfplumber` contra o arquivo real da Bradesco — `page.chars`/`page.extract_text()` vêm vazios em toda página, porque o documento é uma composição de imagens raster (cada linha da tabela é literalmente um bitmap), sem nenhuma camada de texto. Nesse caso o parser usa `docling` (biblioteca de OCR + reconstrução de estrutura de tabela, adicionada ao `requirements.txt` — pesada: traz `torch`/`transformers`/`opencv-python` como dependência transitiva, então o primeiro `pip install` baixa bem mais do que os parsers em `pdfplumber` exigiam) em vez de `pdfplumber`. Ver o docstring de `operadoras/bradesco/saude.py` para o motivo de usar reconstrução de tabela (`DocumentConverter().convert(...).document.tables`, cabeçalho identificado por texto normalizado via `_classifica_coluna`, não por posição fixa) e o contorno de um bug real de fronteira de célula do modelo de tabela (TableFormer) nas colunas numéricas estreitas — valor de uma linha "vazando" pra célula da linha vizinha, contornado extraindo todos os valores monetários da área em ordem de leitura e redistribuindo 1 por linha, em vez de confiar em qual célula específica o modelo atribuiu cada valor. Ao adicionar outra operadora nesse mesmo caso (PDF sem texto selecionável), reaproveitar essa técnica em vez de assumir que `pdfplumber` vai funcionar — testar primeiro com `page.chars`/`extract_text()` contra o arquivo real antes de escolher qual dos dois usar.
**Bradesco Dental / "Bradessaude" Odonto (3759)** — mesmo código de operadora (`CODIGOOUTEMP`) que já existia como "ODONTOPREV S.A." na planilha padrão; o boleto da própria operadora avisa que é o mesmo plano, "antes cobrado como Odontoprev e agora identificado temporariamente como Bradsaude". PDF "SPG/Grupos Especiais - Bradesco Dental - Fatura Técnica" — página 1 é sempre o boleto (sem beneficiário nenhum), a tabela de beneficiários vem a partir da página 2, páginas finais são só o texto legal "MENSAGENS". Titular/dependente vem da coluna "Certif." (`<família>/00` = titular, `<família>/01`, `/02`... = dependente), casamento por nome (sem CPF no arquivo) — mesmo desenho da Bradesco Saúde. Particularidade própria: um mesmo beneficiário pode gerar várias linhas de lançamento por movimentação retroativa (inclusão/cancelamento com efeito em meses anteriores, códigos CM/CR/IR/IM), cada uma com seu próprio Mês/Ano e Valor — todas somadas por indivíduo, igual à regra geral de "somar todas as rubricas do mesmo indivíduo". **Ressalva importante**: ao contrário dos demais parsers deste pacote, este foi escrito só a partir do texto de um PDF colado numa conversa (o arquivo nunca chegou a ficar disponível em disco pra rodar `pdfplumber`/`docling` de verdade) — a extração via `pdfplumber` foi validada batendo a soma dos valores e a contagem de lançamentos contra o resumo do próprio boleto (37 lançamentos, R$ 949,05), mas **ainda precisa ser confirmada rodando o parser contra o arquivo real** (botão "Selecionar arquivo" da tela de Nova Importação já faz isso antes de qualquer coisa ser persistida) — se a extração vier vazia, é sinal de que este PDF também precisa de OCR via `docling`, como a Bradesco Saúde.
**Diferença deliberada em relação ao pipeline original**: lá, o valor do mês sempre gravava na coluna `VALOR` (desconto do empregado), nunca em `VALOREMPRESA` — regra fixa. Aqui, o usuário escolhe na tela de nova importação, **por tipo de lançamento (mensalidade/coparticipação) e por tipo de beneficiário (titular/dependente)** — quatro combinações independentes, ex.: mensalidade do titular custeada pela empresa e mensalidade do dependente descontada do empregado —, uma de três regras de custeio: "Custeado pela empresa" (`{"modo": "empresa"}`), "Descontado do empregado" (`{"modo": "empregado"}`, o comportamento antigo — nomenclatura "empregado", não "funcionário", pra não confundir com `NOMEFUNC`/`CPFFUNC` do leiaute do Questor, que é outra coisa) ou "Regra específica" (`{"modo": "especifica", "limite_valor": float|None, "percentual": float|None}`). Na regra específica, `limite_valor` é um teto de quanto a empresa cobre (o excedente vira desconto do empregado) e `percentual` é a fração do valor do mês custeada pela empresa (o resto vira desconto) — o usuário pode preencher só um dos dois ou os dois juntos; quando os dois vêm preenchidos, prevalece o que resultar no **menor** valor custeado pela empresa (mais restritivo), decisão explícita do usuário. Essa divisão é calculada por `matcher._calcula_valores(valor_total, regra)` (chamada por `_aplica_regra_custeio`, que grava `valor_empresa`/`valor` **os dois juntos** a partir do mesmo `valor_total`) — note que `valor_empresa` é arredondado primeiro e `valor` é derivado como o complemento exato (`valor_total - valor_empresa`, também arredondado), nunca os dois arredondados de forma independente, senão a soma dos dois podia ficar 1 centavo a mais/menos que o valor original (ex.: 50% de 51,69 tem que fechar em 25,84 + 25,85 = 51,69, não 25,85 + 25,85). Qual das duas regras (titular ou dependente) usar em cada `Individuo`/`LinhaSistema` é resolvido por `matcher._regra_para_pessoa(regra_por_pessoa, tipo_pessoa)` — `tipo_pessoa` 'T' cai em "titular", 'D'/'A' caem em "dependente" (mesmo critério de "D e A tratados igual" já usado no resto do leiaute) — chamada nos dois pontos de aplicação de `_casa_por_cpf`/`_casa_por_nome` antes de `_aplica_regra_custeio`.
### Modelos (`portal_api/models.py`)
@ -594,30 +597,47 @@ Antes de existir isso, os dois arquivos (planilha padrão + arquivo da operadora
Gerar o arquivo é um download binário (CSV ou ZIP), não JSON — por isso `pidGerarArquivoPlanoSaude()` não usa `pidApiRequest` (que sempre tenta `JSON.parse`); faz um `fetch` manual reaproveitando `pidEnsureCsrfCookie`/`pidGetCookie`/`pidErrorMessageFrom` de `api.js` (funções globais na página) e dispara o download via `URL.createObjectURL`.
### Regras de custeio salvas
### Cadastro de Regras (separado da execução da importação)
Substituiu o antigo par de botões "Exportar regra"/"Importar regra" (baixava/lia um `.json` manualmente, sem nenhuma persistência) por um banco de regras de verdade no Postgres (`RegraCusteioPlanoSaude`) — pedido explícito do usuário pra poder nomear uma regra (ex.: "092 - Unimed"), escolhê-la numa lista em importações futuras, editá-la depois e anotar uma observação livre (ex.: "Empresa não desconta plano do empregado XX").
Até uma rodada anterior, o custeio (mensalidade/coparticipação por titular/dependente) era configurado **na hora de importar**, em "Nova Importação" — mesmo aplicando uma regra salva, os campos continuavam livres pra edição ali mesmo. O usuário pediu mais segurança operacional: separar de vez o **cadastro** das regras da **execução**, e atrelar cada regra formalmente a uma empresa (antes era só uma convenção de texto livre no campo `nome`, ex. `"092 - Unimed"`, sem nenhum campo estruturado). Duas telas agora:
- **Campos**: `nome` (obrigatório), `operadora` (opcional — a `key` de `planos_saude.pipeline.OPERADORAS`, não o label; só usada pra pré-selecionar o `<select>` de operadora ao aplicar a regra, nunca bloqueia aplicar uma regra com uma operadora diferente da atual), `tipos_lancamento`/`custeio_por_tipo` (exatamente o mesmo formato dos campos homônimos de `ImportacaoPlanoSaude`, ver acima) e `observacoes` (texto livre).
- **Validação reaproveitada, não duplicada**: `RegraCusteioPlanoSaudeSerializer.validate()` e `ImportacaoPlanoSaudeCreateSerializer.validate()` chamam a mesma função módulo-level `_monta_regra_custeio()` (`serializers.py`) pra validar/parsear cada combinação tipo×pessoa — sem isso, a regra de negócio de custeio (parsing BR, faixa 0–100 do percentual, "ao menos um de limite/percentual") viveria duplicada em dois serializers e podia divergir com o tempo. A única diferença entre os dois pontos de entrada é o formato de payload: `ImportacaoPlanoSaudeCreateSerializer` recebe campos multipart achatados (`custeio_mensalidade_titular`, `limite_valor_mensalidade_titular`...), `RegraCusteioPlanoSaudeSerializer` recebe o `custeio_por_tipo` já aninhado como JSON puro.
- **`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.
- **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".
- **"Cadastro de Regras"** (botão na lista principal, ao lado de "+ Nova Importação", abre `#ips-regracad-modal`) — único lugar onde uma `RegraCusteioPlanoSaude` é criada ou editada. "Empresa" (`#ips-regracad-empresa-combo`, códigos distintos entre as regras já cadastradas, mostrando `"<código> - <nome>"` — ver "Nome da empresa (Questor)" abaixo) numa linha própria, com "Operadora" (`#ips-regracad-operadora-combo`, restrito às operadoras com regra pra a empresa escolhida) numa linha abaixo — decisão explícita do usuário, pra o nome da empresa não competir visualmente com a operadora. Os dois comboboxes têm dois botões embutidos na própria barra (ver detalhe em "Nova Importação" abaixo): o "x" pra limpar (`.ips-combo__clear`, só aparece com algo selecionado) e uma seta "▾" (`.ips-combo__toggle`, sempre visível) pra ver de novo a lista completa/as outras opções. Limpar Empresa também limpa Operadora automaticamente (dispara o mesmo `onChange` de quando a empresa é trocada). Como há **no máximo uma regra por combinação empresa+operadora** (`unique_together`, ver abaixo), escolher os dois já resolve a regra pra edição in-place, com "Salvar alterações"/"Excluir regra" — depois de salvar/excluir com sucesso, o modal **fecha** (decisão explícita do usuário; antes continuava mostrando a regra editada). Botão "+ Nova regra" (`#ips-regracad-nova-btn`, ao lado de Empresa) alterna pro modo criação: campo de texto livre "Código da empresa" (`#ips-regracad-novo-codigo-empresa`) com o nome resolvido do Questor ao lado (`#ips-regracad-novo-empresa-nome`, ver "Nome da empresa (Questor)" abaixo) + combobox "Operadora" sem restrição numa linha abaixo (`#ips-regracad-novo-operadora-combo`, catálogo completo de `pipeline.OPERADORAS`) + o mesmo bloco de custeio vazio + "Criar regra" (fecha o modal também, ao concluir). Um segundo botão "+ Nova operadora" (`#ips-regracad-nova-operadora-btn`, ao lado do combobox de Operadora da navegação, só visível quando uma empresa já está selecionada) atalha pro mesmo modo de criação, com o código da empresa já pré-preenchido — pensado pra "essa empresa já tem regra, mas não pra essa operadora".
- **Indicador de modo** (`#ips-regracad-modo`, pedido explícito do usuário pra nunca confundir "editando" com "criando"): mostra "Editando regra existente: `<código - nome>` · `<operadora>`" (`regracadCarregarParaEdicao()`) ou "Cadastrando regra nova" (`regracadEntrarModoNovo()`) — nada, no estado vazio (`regracadMostrarVazio()`).
- **A barra de navegação (Empresa/Operadora) some no modo "+ Nova regra"** (`#ips-regracad-toolbar`, `hidden` alternado por essas mesmas três funções) — evita mostrar as duas seções (navegação + criação) ao mesmo tempo, o que confundia qual das duas estava "valendo". Um botão **"Cancelar"** (`#ips-regracad-novo-cancelar-btn`, só visível nesse modo) volta pra navegação (`regracadCancelarNovo()` → `regracadMostrarConformeSelecaoAtual()`, que reexibe a regra que estava sendo vista antes, se alguma) sem fechar o modal inteiro — diferente de "Fechar".
- **Aviso de duplicidade em "+ Nova regra"** (`#ips-regracad-novo-operadora-duplicada`, `regracadAtualizarNovoOperadoraDuplicada()`, chamada a cada mudança de código ou de operadora): se a combinação já tiver uma regra cadastrada, mostra "Já existe uma regra cadastrada para esta empresa com esta operadora..." abaixo do combobox de Operadora e desabilita "Criar regra" — evita a viagem de ida e volta até a validação do backend (que também recusa, via `unique_together`) pra descobrir o mesmo problema.
- **Reabrir o modal nunca mostra o estado anterior por um instante**: `abrirCadastroRegras()` chama `regracadMostrarVazio()` de forma síncrona, antes de qualquer `await` (bug real corrigido — antes a limpeza só rodava depois das buscas de operadoras/regras, e o modal reabria mostrando por um instante o que estava na tela antes de ter sido fechado).
- **"Nova Importação"** (formulário de execução) ficou **só leitura** pra custeio: "Empresa" (`#ips-imp-empresa-combo`, mesma fonte do Cadastro, mesmo `"<código> - <nome>"`) e "Operadora" (`#ips-imp-operadora-combo`, restrito à empresa escolhida, cada um numa linha própria) resolvem a única regra da combinação (`regraResolvidaAtual`, JS) e mostram um **resumo só-leitura** (`#ips-imp-resumo` — tipos cobertos, custeio de mensalidade/coparticipação, observações), sem nenhum campo editável. Nenhuma empresa aparece nesse combobox sem já ter uma regra cadastrada — cadastrar/editar uma regra pra uma empresa nova é sempre um passo anterior, feito em "Cadastro de Regras". Na tela de Revisão, `#ips-review-empresa` (ao lado do título "Revisão") mostra `"<código> - <nome>"` da empresa sendo importada, pra identificar de cara sem precisar abrir a aba de linhas. Os dois comboboxes (aqui e nos três de "Cadastro de Regras") têm dois botões embutidos na própria barra: o "x" (`.ips-combo__clear`, só aparece com algo selecionado/digitado) e uma seta "▾" (`.ips-combo__toggle`, sempre visível, mesma posição de um `<select>` nativo) — clicar na seta mostra a lista completa de novo, ou, se já houver algo selecionado, as **outras** opções cadastradas (sem repetir a já escolhida). Existe porque só focar o campo com um valor já preenchido filtra a lista pelo texto atual, então só mostraria de novo o item já selecionado — a seta é o jeito de "trocar fácil" pedido pelo usuário, no mesmo espírito de um filtro de BI (clicar, ver todas as opções, escolher outra).
O bloco de checkboxes/radios de custeio (`.ips-tipo-field`, mensalidade/coparticipação × titular/dependente/regra específica) e as funções JS que o operam (`coletarCusteioAtual()`, `mensagemErroCusteio()`, `aplicarCusteio()`, `limparCusteioForm()`) foram **movidos** (não duplicados) de "Nova Importação" pro modal de Cadastro — mesmos ids de DOM, mesma lógica, só relocados; "Nova Importação" monta o `FormData` do submit direto a partir do objeto `regraResolvidaAtual` em memória (`montarFormDataDeRegra()`), não mais lendo inputs (que não existem mais ali).
- **Campos de `RegraCusteioPlanoSaude`**: `codigo_empresa` (obrigatório — o código do cliente/empresa; **não confundir** com o código de cadastro da operadora no Questor, que já aparece dentro do label de `pipeline.OPERADORAS`, ex. `"5060 - Unimed Saúde"` — são códigos diferentes), `operadora` (obrigatória agora, validada contra `pipeline.OPERADORAS`), `regra_empresa_chave` (ver "Regra empresa" abaixo), `tipos_lancamento`/`custeio_por_tipo` (mesmo formato dos campos homônimos de `ImportacaoPlanoSaude`) e `observacoes`. `Meta.unique_together = [["codigo_empresa", "operadora"]]` — validado contra os 12 registros reais existentes antes de impor a restrição (nenhuma combinação se repetia). `nome` **deixou de ser digitado** pelo usuário — é sempre derivado em `RegraCusteioPlanoSaudeSerializer.validate()` como `"<codigo_empresa> - <nome da operadora sem o código dela>"` (campo `read_only=True` na API); mantido como campo de model só pra não precisar tocar em todo lugar que já lê `.nome`/`regra_custeio_salva_nome`.
- **Migração em 3 passos** (mesmo padrão já usado pra `IndicadorDepartamento`, migrations `0033`/`0034`/`0035`): `0041` adiciona `codigo_empresa`/`regra_empresa_chave` (blank) + torna `operadora` obrigatória; `0042` (RunPython) faz o backfill de `codigo_empresa` a partir do `nome` existente (`nome.split(" - ", 1)[0].strip()`); `0043` torna `codigo_empresa` obrigatório e adiciona o `unique_together`. `Meta.ordering` usa `[Length("codigo_empresa"), "codigo_empresa", "operadora"]` (mesmo padrão de `IndicadorApuracaoEmpresa`) pra ordenar o código como número, não como string.
- **Validação reaproveitada, não duplicada**: `RegraCusteioPlanoSaudeSerializer.validate()` e `ImportacaoPlanoSaudeCreateSerializer.validate()` continuam chamando a mesma função módulo-level `_monta_regra_custeio()` (`serializers.py`) pra validar/parsear cada combinação tipo×pessoa. `UniqueTogetherValidator` é declarado explicitamente em `Meta.validators` (não só o automático do DRF), pra manter a mensagem de erro em português.
- **`ImportacaoPlanoSaude.regra_custeio_salva`** (FK opcional, `SET_NULL`) registra qual regra foi aplicada numa importação — agora praticamente sempre preenchida (já que "Nova Importação" só resolve custeio a partir de uma regra cadastrada), mas o campo continua opcional a nível de API (a garantia de "sempre passar por uma regra cadastrada" é uma trava de UI, não uma obrigatoriedade no backend). Alimenta `regra_custeio_salva_nome`/`regra_custeio_salva_observacoes` na tela de Revisão, como antes.
- **Trava de conferência do código de empresa** (`ImportacaoPlanoSaudeViewSet.create()`, depois do processamento e antes do `bulk_create` das linhas): se `regra_custeio_salva` está presente, confere que ao menos uma linha da planilha padrão processada tem `codigo_empresa` igual ao da regra; se não bater, desfaz a importação (mesmo padrão de cleanup dos outros `except` desse método) e devolve 400 com mensagem clara — evita aplicar a regra de uma empresa a uma planilha de outra por engano. Vale pra toda regra aplicada, inclusive as com `regra_empresa_chave` (onde é redundante com a checagem que `regras_empresa.valida_regra_empresa()` já faz — proteção extra contra o registro em `REGRAS_EMPRESA` ficar dessincronizado da `RegraCusteioPlanoSaude` correspondente).
**Nome da empresa (Questor)** — primeiro consumidor real do pacote `database/` (ver [[project_database_package]] na memória): resolve e cacheia localmente o nome de uma empresa a partir do seu `codigo_empresa`, pra mostrar `"<código> - <nome>"` em vez de só o código nas telas acima.
- **`EmpresaQuestor`** (models.py, migração `0044`): `codigo_empresa` (único) + `nome_empresa`, um cache local simples — sem relação de FK com `RegraCusteioPlanoSaude` (é uma propriedade da empresa, não da regra; várias regras podem compartilhar o mesmo `codigo_empresa` com operadoras diferentes, ex. "221" com Bradesco/Itamed/Unimed, e todas reaproveitam a mesma linha de `EmpresaQuestor`).
- **`portal_api/empresas_questor.py`, `resolve_nome_empresa(codigo_empresa)`**: olha o cache primeiro; só na ausência dele consulta o Questor (`database.connection.DatabaseConnection("questor")` — chave em **minúsculas**, `DatabaseSettings` normaliza as chaves de `DATABASE__<NOME>__*` do `.env` assim, ao contrário do que o padrão de nomenclatura das próprias env vars sugere) executando `sqls.questor.QuestorSQL.consulta_nome_empresa()` (`select codigoempresa, nomeempresa from empresa where codigoempresa = :codigo_empresa` — a consulta exata fornecida pelo usuário, só parametrizada), e persiste o resultado antes de devolver — nunca precisa repetir a consulta pro mesmo código depois. Qualquer falha (código inexistente, `codigoempresa` do Questor é `smallint` e um código fora da faixa numérica levanta `DataError`, banco inacessível) é capturada e devolve `None` — nunca propaga a exceção, já que isso é só informativo, nunca bloqueia cadastrar/editar/excluir uma regra.
- **`normalizar_codigo_empresa(valor)`** (mesmo arquivo): remove zero à esquerda (`"092"` → `"92"`) — decisão explícita do usuário, pra sempre ter um único código canônico por empresa (o `codigoempresa` do Questor é `smallint`, então "092"/"92" já eram a mesma linha lá; sem normalizar no Portal, apareciam como duas empresas "diferentes"). Aplicada em toda entrada de `codigo_empresa` vinda de fora: `resolve_nome_empresa()`, `RegraCusteioPlanoSaudeSerializer.validate_codigo_empresa()` (o que é de fato salvo em `RegraCusteioPlanoSaude.codigo_empresa`), a action `nome-empresa` (devolve o código já normalizado, pro frontend reescrever o campo), e a trava de conferência em `ImportacaoPlanoSaudeViewSet.create()` (normaliza os dois lados antes de comparar, já que o código bruto da planilha pode ter zero à esquerda enquanto o da regra não tem mais). **Nunca** aplicada a `ImportacaoPlanoSaudeLinha.codigo_empresa` em si (precisa continuar exatamente como veio da planilha, pra não alterar o que é reexportado) — só normalizada no momento de uma comparação/exibição pontual (ver `_nome_empresa_cacheado()`, que normaliza antes de consultar `EmpresaQuestor` a partir do código cru de uma linha).
- **`sqls/questor.py`** (pacote novo na raiz do projeto, ao lado de `database/` — seguindo a convenção "uma pasta `sqls/` por projeto consumidor, um arquivo por banco" já documentada na memória): classe `QuestorSQL`, hoje só `consulta_nome_empresa()`. Adicionar uma consulta nova ao Questor/Tareffa segue o mesmo padrão — método estático devolvendo `SQLQuery(sql=dedent(...), params={...})`; **nunca** usar `execute`/`execute_returning` desses bancos sem autorização explícita (ver [[feedback_bancos_externos_somente_leitura]]).
- **Correção em `database/settings.py`** (arquivo compartilhado, não específico desta ferramenta): `SUPPORTED_DRIVERS["postgresql"]` apontava pra `"postgresql+psycopg2"`, mas o `.venv` do Portal só tem `psycopg` (v3) instalado, não `psycopg2` — `ModuleNotFoundError` ao tentar conectar. Corrigido pra `"postgresql+psycopg"` (dialeto psycopg3 do SQLAlchemy), reaproveitando a dependência que já existe em vez de instalar `psycopg2-binary` à parte. Se `database/` for reaproveitado por outro projeto que dependa especificamente de `psycopg2` (comportamento antigo), essa mudança precisaria ser revisitada — não é o caso hoje.
- **Resolução automática pra regras já existentes**: `RegraCusteioPlanoSaudeSerializer.get_nome_empresa()` chama `resolve_nome_empresa()` a cada leitura (não só ao criar/editar) — então regras cadastradas antes deste campo existir tiveram o nome resolvido e cacheado sozinho, na primeira vez que a lista foi carregada depois do deploy, sem precisar de nenhum backfill manual. Já `ImportacaoPlanoSaudeDetailSerializer.get_nome_empresa()` (tela de Revisão) só lê o cache (`_nome_empresa_cacheado()`, sem chamar `resolve_nome_empresa()`) — essa tela é consultada com muito mais frequência, e o nome já deveria estar cacheado desde que a regra foi cadastrada/editada, então não vale pagar o custo de uma consulta ao Questor ali.
- **Frontend** (`importacao-plano-saude.js`): `GET /api/regras-custeio-plano-saude/nome-empresa/?codigo_empresa=X` (`pidBuscarNomeEmpresaPlanoSaude`) é chamado tanto num debounce de 350ms a cada tecla digitada no campo "Código da empresa" de "+ Nova regra" (`agendarAtualizarNomeEmpresaNovo()`, pedido explícito do usuário pra não precisar esperar o campo perder o foco) quanto no `blur` (imediato, cancela o debounce pendente) — a função de fato (`atualizarNomeEmpresaNovo()`) mostra "Buscando nome da empresa...", depois o nome resolvido ou "Empresa não encontrada no Questor.", e **reescreve o próprio campo** com o `codigo_empresa` normalizado devolvido pela resposta (ex.: usuário digita "092", campo passa a mostrar "92" assim que resolve). Os comboboxes de "Empresa" (Cadastro de Regras e Nova Importação) não fazem nenhuma chamada nova — `labelEmpresa()` monta `"<código> - <nome>"` direto do array `regras` já carregado, que já vem com `nome_empresa` resolvido pelo backend.
### 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.
Cobre regras de custeio negociadas com uma empresa específica que não cabem no desenho normal "por tipo de lançamento × titular/dependente", tipicamente porque são calculadas por **família inteira** (titular + todos os dependentes somados), não por pessoa. O algoritmo em si (`portal_api/planos_saude/regras_empresa.py`, `REGRAS_EMPRESA: Dict[str, dict]`) continua sendo um registro fixo no código, cadastrado pelo desenvolvedor quando o cliente repassa uma regra nova — mas **desde a rodada do Cadastro de Regras separado, a escolha de USAR uma regra especial deixou de ser um checkbox independente na tela de importação e passou a viver dentro do cadastro por empresa+operadora**: o campo `RegraCusteioPlanoSaude.regra_empresa_chave` (chave de `REGRAS_EMPRESA`) é configurado uma vez, junto do resto do custeio, no modal "Cadastro de Regras" — "Nova Importação" só resolve o que já foi cadastrado, sem checkbox próprio.
- **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), `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).
- **Mutuamente exclusivo com custeio manual de mensalidade, agora por campo da regra**: no formulário de Cadastro de Regras, marcar "Mensalidade usa regra especial da empresa" (`#ips-form-tipo-regra-empresa`, ids preservados do checkbox antigo) desmarca e esconde os radios titular/dependente de Mensalidade (e vice-versa) — mesma exclusividade de antes, só que dentro do cadastro em vez de na execução. No backend, `RegraCusteioPlanoSaudeSerializer.validate()` exige `"mensalidade"` em `tipos_lancamento` quando `regra_empresa_chave` é preenchida, confere que `REGRAS_EMPRESA[chave]["codigo_empresa"]`/`["operadora"]` batem com os da própria regra (trava contra vincular o algoritmo de uma empresa a outra por engano) e zera `custeio_por_tipo["mensalidade"]`. No submit de "Nova Importação", `montarFormDataDeRegra()` envia `regra_empresa=<chave>` a partir de `regraResolvidaAtual.regra_empresa_chave` — o restante do pipeline (`ImportacaoPlanoSaudeCreateSerializer.validate()`, `pipeline.processa_importacao`, `regras_empresa.valida_regra_empresa`) **não mudou**, só a origem do valor no frontend.
- **Registro** (`REGRAS_EMPRESA`): cada entrada tem `label`, `codigo_empresa` (código da empresa na planilha padrão pra qual a regra foi negociada), `operadora`, `aplica` (função que faz o cálculo) e `observacoes`. Pra cadastrar uma regra nova: escrever a função e registrar aqui — nada mais precisa mudar (`GET /api/importacoes-plano-saude/regras-empresa/` já reflete o registro, consumido tanto pelo seletor dentro do Cadastro de Regras quanto pelo resumo só-leitura de "Nova Importação").
- **Observações da regra, só-leitura na tela de Revisão** (`#ips-review-regra-empresa-obs`) — inalterado: `ImportacaoPlanoSaudeDetailSerializer.regra_empresa_observacoes` resolve `REGRAS_EMPRESA[obj.regra_empresa]["observacoes"]` a cada carregamento; o mesmo bloco cai pra `regra_custeio_salva_observacoes` quando não há regra empresa.
- **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.
- **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 (a mesma ideia da trava geral descrita acima, só que específica pra este mecanismo e mais antiga).
- **`_aplica_teto_familia` é duck-typed de propósito** (`linhas_e_valores: List[Tuple[Any, float]]`, `_eh_linha_titular()` própria em vez de `LinhaSistema.eh_linha_titular()`): roda tanto contra `LinhaSistema` (pipeline, na criação da importação) quanto contra `ImportacaoPlanoSaudeLinha` (model Django, no recálculo pós "Vincular pessoa" — ver `views._recalcula_familia_regra_empresa` e "Resolução manual de auditoria por nome" acima) — as duas classes têm os mesmos atributos de string (`nome_dependente`/`cpf_dependente`/`valor_empresa`/`valor`), só a segunda não tem o método `eh_linha_titular()`.
- **Coparticipação nunca é afetada**: "Regra empresa" só cobre `"mensalidade"`; se o usuário também marcar "Coparticipação", ela segue o custeio normal configurado na própria tela (radios titular/dependente), sem nenhuma ligação com a regra empresa.
- **Frontend** (`importacao-plano-saude.js`): checkbox "Regra empresa" revela uma caixa (`#ips-regra-empresa-box`) com o nome da regra atualmente escolhida + botão "Selecionar regra", que abre um modal simples (`#ips-regra-empresa-modal`, lista `.ips-regra-row` sem "Excluir"/"Editar" — é um registro fixo) buscado de `GET /operadoras/regras-empresa/` uma vez por abertura do formulário (`regrasEmpresaCache`). `regraEmpresaSelecionada` (JS, `{key, label}`) é lido no submit (`formData.append("regra_empresa", ...)`) e em `mensagemErroCusteio()` (exige uma regra escolhida se o checkbox estiver marcado). `limparCusteioForm()`/`limparRegraEmpresa()` resetam o checkbox/caixa/seleção junto com o resto do custeio — inclusive quando "Limpar seleção" da regra de custeio salva é clicado, ou quando uma regra de custeio salva é aplicada (`aplicarCusteio()` sempre desliga "Regra empresa" antes de configurar mensalidade manualmente).
- **Coparticipação nunca é afetada**: `regra_empresa_chave` só cobre `"mensalidade"`; se a regra também cobrir `"coparticipacao"`, ela segue o custeio normal configurado no mesmo cadastro (radios titular/dependente), sem nenhuma ligação com a regra empresa.
## CSS — organização entre arquivos

View File

@ -8,7 +8,7 @@ BASE_DIR = Path(__file__).resolve().parents[2]
ENV_FILE_PATH = BASE_DIR / ".env"
SUPPORTED_DRIVERS = {
"postgresql": "postgresql+psycopg2",
"postgresql": "postgresql+psycopg",
"mysql": "mysql+pymysql",
"sqlserver": "mssql+pyodbc",
"sqlite": "sqlite"

Binary file not shown.

View File

@ -0,0 +1,17 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
1684;SHIRLEY BAPTISTA BASSO BENITEZ;005.806.599-76;3755;01/01/2026;LAURA BASSO BENITEZ;150.028.919-18;0;0;
1684;SHIRLEY BAPTISTA BASSO BENITEZ;005.806.599-76;3755;01/01/2026;ANGELO MATHEUS BASSO BENITEZ;150.028.879-96;0;0;
1684;SHIRLEY BAPTISTA BASSO BENITEZ;005.806.599-76;3755;01/01/2026;;;0;0;
1684;LUIZ AGNALDO NOGOCEKI;007.397.429-30;3755;01/01/2026;;;0;0;
1684;ANA PAULA FABRI KONART;031.029.339-12;3755;01/01/2026;;;0;0;
1684;LUCIANA SEVERO SCHITZ;044.810.799-67;3755;01/05/2026;GABRIEL SEVERO SCHITZ;097.527.819-34;0;0;
1684;LUCIANA SEVERO SCHITZ;044.810.799-67;3755;01/05/2026;;;0;0;
1684;LUCIANA SEVERO SCHITZ;044.810.799-67;3755;01/05/2026;DAVI SEVERO SCHITZ;137.347.779-25;0;0;
1684;CLEIDIANE CASAGRANDE MACHADO;054.236.429-81;3755;01/01/2026;;;0;0;
1684;CLEIDIANE CASAGRANDE MACHADO;054.236.429-81;3755;01/01/2026;MIGUEL CASAGRANDE MACHADO;131.884.309-06;0;0;
1684;MARCO ANTONIO MERTIG DRESLING DESIDERIO;062.891.039-89;3755;01/01/2026;;;0;0;
1684;DOUGLAS WILLIAN DE MELO;081.295.649-47;3755;01/06/2024;MAIARA CRISTINA DOS SANTOS VICENTE;077.993.259-50;0;0;
1684;DOUGLAS WILLIAN DE MELO;081.295.649-47;3755;01/06/2024;;;0;0;
1684;TATIANE DA SILVA;322.352.038-41;3755;01/01/2026;PEDRO HENRIQUE SILVA GONCALVES;152.166.939-20;0;0;
1684;TATIANE DA SILVA;322.352.038-41;3755;01/01/2026;;;0;0;
1684;LEILA DA SILVA VEIGA;931.427.909-00;3755;01/01/2026;;;0;0;
Internal Server Error - De Paula Contadores — Git

Internal Server Error

Gitea Version: 1.22.3