Duplicação da Aplicação Importação Plano de Saúde para o RH

This commit is contained in:
Gabriel 2026-08-28 17:30:31 -03:00
parent 51bc04ef6f
commit 0bac4d41f8
24 changed files with 2455 additions and 29 deletions

View File

@ -52,22 +52,16 @@ Configurar as variáveis de ambiente do Postgres 14 antes de migrar — `config/
``` ```
python manage.py makemigrations portal_api python manage.py makemigrations portal_api
python manage.py migrate python manage.py migrate
python manage.py seed_portal # recria os 8 perfis + usuários gabriel/bruno
python manage.py runserver python manage.py runserver
``` ```
Abrir `http://localhost:8000/` — o Django serve `index.html` e as demais páginas do frontend diretamente (ver `config/urls.py`), então **não há mais um segundo servidor** (`python -m http.server`) para o frontend.Vamos desenvolver outra **`python manage.py seed_portal` é só para ambiente novo/vazio (primeira criação do banco)** — não rodar mais neste ambiente. **O Portal já está em produção**: os 8 perfis de acesso e todas as contas de usuário (incluindo `gabriel`/`bruno`) já existem de verdade, com senhas e permissões mantidas pelos próprios usuários direto pela tela (Perfis de Acesso/Usuários) — não mais pelo seed. `seed_portal` resincroniza incondicionalmente `permissoes`/`ativo`/`gerencia_permissoes` dos 8 perfis de código fixo a partir de `catalogo.py` a cada execução (ver "Cuidado com `seed_portal.py`" mais abaixo) — rodar isso contra o banco já em uso reverteria qualquer permissão que um admin tenha customizado manualmente para um desses 8 perfis, sem nenhum aviso. Ao adicionar uma aplicação nova ao catálogo (novo `app_key`), a chave nova ainda entra automaticamente pros perfis certos na próxima vez que `seed_portal` rodar — mas evitar rodar só por causa disso; se for mesmo necessário, avisar o usuário antes e confirmar, e idealmente conferir os 8 perfis em Perfis de Acesso depois pra garantir que nenhuma customização foi perdida.
Contas de demonstração (criadas por `seed_portal`, senha via `set_password` do Django — não é mais texto puro):
- `gabriel` / `gabriel` — perfil "Integração e Inovação" (código 8), o único com `gerencia_permissoes=True` (acesso total + gerencia Perfis de Acesso/Usuários) e o único com os `*-editar` de `links-ferramentas` (cartões e Acessos Gerais) e de `ramais` `True` (único perfil que pode reordenar/incluir/remover cartões em Links & Ferramentas, criar/editar seções e acessos em Acessos Gerais, e adicionar/editar ramal/criar ausência em Ramais, até que outro perfil seja liberado em Perfis de Acesso — ver "Modelo de permissões" abaixo).
- `bruno` / `bruno` — sem perfil vinculado, usado para testar o estado "sem acesso".
Não há suíte de testes, lint ou build configurados neste projeto. Não há suíte de testes, lint ou build configurados neste projeto.
### Ambiente de desenvolvimento assistido ### Ambiente de desenvolvimento assistido
O `.venv` do projeto já tem Python 3.13 + Django 6.0 + DRF + psycopg + python-dotenv + Pillow + nh3 + reportlab + openpyxl + holidays + docling instalados, e há um Postgres local acessível via `.env` — dá pra rodar `makemigrations`/`migrate`/`seed_portal`/`runserver` normalmente por aqui usando `.venv\Scripts\python.exe manage.py ...` (ou ativando o venv primeiro). Isso deixou de ser uma limitação a partir da rodada em que o ambiente ganhou essas ferramentas (ver `plano.md`) — não assumir mais que só é possível revisar o backend estaticamente. O `.venv` do projeto já tem Python 3.13 + Django 6.0 + DRF + psycopg + python-dotenv + Pillow + nh3 + reportlab + openpyxl + holidays + docling instalados, e o Postgres acessível via `.env` é o **banco de produção** (não uma cópia de desenvolvimento) — dá pra rodar `makemigrations`/`migrate`/`runserver` normalmente por aqui usando `.venv\Scripts\python.exe manage.py ...` (ou ativando o venv primeiro), mas **nunca `seed_portal`** (ver "Como rodar / testar localmente" acima — recria/ressincroniza perfis já em uso de verdade). Isso deixou de ser uma limitação a partir da rodada em que o ambiente ganhou essas ferramentas (ver `plano.md`) — não assumir mais que só é possível revisar o backend estaticamente.
## Arquitetura ## Arquitetura
@ -294,6 +288,8 @@ Isso reaproveita 100% a árvore de permissões que já existe (`renderTree()`/`r
**Cuidado com `seed_portal.py`**: o módulo continua tendo seu próprio `enabled` (visibilidade no menu) e, se estiver em `BASE_KEYS`/`SECTORAL_KEYS` etc., `catalogo.permissions_from_keys()` habilita **todos** os apps de um módulo enabled de uma vez — incluindo todo `*-editar`. Por isso `seed_portal.py` força `permissoes["links-ferramentas"]["apps"]["links-ferramentas-editar"] = False`, `permissoes["links-ferramentas"]["apps"]["acessos-gerais-editar"] = False` e `permissoes["ramais"]["apps"]["ramais-editar"] = False` (+ as outras duas subtelas administráveis de Ramais) explicitamente pra todo perfil que não seja "Integração e Inovação" (código 8, `chaves=None` → todos os apps `True` de propósito), depois de gerar o dict — sem esse override, qualquer perfil com o módulo habilitado nasceria com poder de editar tudo. Ao adicionar uma aplicação nova nesse padrão (dentro de um módulo existente ou não), replicar esse mesmo cuidado no seed. **Cuidado com `seed_portal.py`**: o módulo continua tendo seu próprio `enabled` (visibilidade no menu) e, se estiver em `BASE_KEYS`/`SECTORAL_KEYS` etc., `catalogo.permissions_from_keys()` habilita **todos** os apps de um módulo enabled de uma vez — incluindo todo `*-editar`. Por isso `seed_portal.py` força `permissoes["links-ferramentas"]["apps"]["links-ferramentas-editar"] = False`, `permissoes["links-ferramentas"]["apps"]["acessos-gerais-editar"] = False` e `permissoes["ramais"]["apps"]["ramais-editar"] = False` (+ as outras duas subtelas administráveis de Ramais) explicitamente pra todo perfil que não seja "Integração e Inovação" (código 8, `chaves=None` → todos os apps `True` de propósito), depois de gerar o dict — sem esse override, qualquer perfil com o módulo habilitado nasceria com poder de editar tudo. Ao adicionar uma aplicação nova nesse padrão (dentro de um módulo existente ou não), replicar esse mesmo cuidado no seed.
**Convenção pra aplicação nova que lide com dado sensível/pessoal (não dado de cliente comum)**: decisão explícita do usuário (rodada em que "Importação de Plano de Saúde - De Paula" foi criada, ver `portal_api/planos_saude/CLAUDE.md`) — daqui pra frente, toda aplicação nova adicionada ao catálogo que trate dado pessoal de colaborador do próprio escritório (em vez de dado de cliente, que é o caso comum do resto do Portal) deve **nascer restrita só ao perfil "Inovação"** (código 8), mesmo padrão já usado por "Não Conformidades" (`permissoes["relatorios"]["apps"]["nao-conformidades"] = False` no bloco de overrides) — nunca herdar a visibilidade ampla padrão de `BASE_KEYS`/`SECTORAL_KEYS`. Outros perfis que precisarem ganham acesso depois, manualmente, pela tela de Perfis de Acesso. **Não retroagir** essa convenção nas aplicações de toggle único que já existiam antes desta decisão (Importação de Plano de Saúde original, Simulação de Custo de Contratação, Indicador de Desempenho) — essas continuam com a visibilidade ampla de sempre, decisão explícita do usuário de não mexer no que já está em uso.
**Bug real (rodada 69) — `IntegrityError: duplicate key value violates unique constraint "portal_api_perfilacesso_pkey"` ao criar um perfil pela tela**: `seed_portal.py` semeia `PerfilAcesso` com `codigo` **explícito** (`update_or_create(codigo=dado["codigo"], ...)`, já que os 8 códigos 1–8 são referenciados por número fixo em vários lugares do código — ex.: `codigo == 8` = "Integração e Inovação"). No Postgres, um `INSERT` com PK explícita **nunca avança a sequence** por trás do `AutoField` — então a sequence ficava parada em 1 (seu valor inicial), e o primeiro perfil criado pela tela (`POST /api/perfis/`, sem PK explícita) recebia `codigo=1` do `nextval()`, colidindo com um código já usado pelo seed. `_reset_sequence(model)` (função módulo-level em `seed_portal.py`, roda `SELECT setval(pg_get_serial_sequence(...), MAX(pk))` via SQL puro do Postgres) corrige isso, chamada logo depois do loop de `PERFIS_SEED` — toda vez que `seed_portal` roda, a sequence é realinhada de novo. Se esse erro voltar a aparecer no futuro (ex.: um `loaddata`/`RunPython` de migração também inserindo `PerfilAcesso` com PK explícita sem passar por `seed_portal.py` depois), o comando pra corrigir manualmente é `python manage.py seed_portal` (idempotente, seguro rodar de novo) — não precisa de acesso direto ao banco. **Bug real (rodada 69) — `IntegrityError: duplicate key value violates unique constraint "portal_api_perfilacesso_pkey"` ao criar um perfil pela tela**: `seed_portal.py` semeia `PerfilAcesso` com `codigo` **explícito** (`update_or_create(codigo=dado["codigo"], ...)`, já que os 8 códigos 1–8 são referenciados por número fixo em vários lugares do código — ex.: `codigo == 8` = "Integração e Inovação"). No Postgres, um `INSERT` com PK explícita **nunca avança a sequence** por trás do `AutoField` — então a sequence ficava parada em 1 (seu valor inicial), e o primeiro perfil criado pela tela (`POST /api/perfis/`, sem PK explícita) recebia `codigo=1` do `nextval()`, colidindo com um código já usado pelo seed. `_reset_sequence(model)` (função módulo-level em `seed_portal.py`, roda `SELECT setval(pg_get_serial_sequence(...), MAX(pk))` via SQL puro do Postgres) corrige isso, chamada logo depois do loop de `PERFIS_SEED` — toda vez que `seed_portal` roda, a sequence é realinhada de novo. Se esse erro voltar a aparecer no futuro (ex.: um `loaddata`/`RunPython` de migração também inserindo `PerfilAcesso` com PK explícita sem passar por `seed_portal.py` depois), o comando pra corrigir manualmente é `python manage.py seed_portal` (idempotente, seguro rodar de novo) — não precisa de acesso direto ao banco.
Duas telas administrativas por cima desse modelo (popup "Nova Aplicação" de `perfis-acesso.html`, aba "Usuários do Escritório") estão documentadas em `docs/perfis-usuarios/perfis-usuarios.md`, não aqui. Duas telas administrativas por cima desse modelo (popup "Nova Aplicação" de `perfis-acesso.html`, aba "Usuários do Escritório") estão documentadas em `docs/perfis-usuarios/perfis-usuarios.md`, não aqui.

View File

@ -22,6 +22,7 @@ PAGINAS_FRONTEND = [
"acessos-gerais.html", "acessos-gerais.html",
"ramais.html", "ramais.html",
"importacao-plano-saude.html", "importacao-plano-saude.html",
"importacao-plano-saude-de-paula.html",
"custo-contratacao.html", "custo-contratacao.html",
"indicador-desempenho.html", "indicador-desempenho.html",
"nao-conformidades.html", "nao-conformidades.html",

Binary file not shown.

View File

@ -0,0 +1,15 @@
CODIGOEMPRESA;NOMEFUNC;CPFFUNC;CODIGOOUTEMP;DATAINICIAL;NOMEDEPENDENTE;CPFDEPENDENTE;VALOREMPRESA;VALOR;DESCRICAO
501;ALINE KIEPER;044.273.299-69;3755;01/12/2023;;;0;0;
501;ELIZANGELA DE PAULA KUHN;815.257.459-72;3755;01/12/2023;;;0;0;
501;ELIZANGELA DE PAULA KUHN;815.257.459-72;3755;01/12/2023;JORGE RICARDO KUHN;522.794.640-04;0;0;
501;GABRIELA DE MORAES ALVES DA SILVA;106.626.889-45;3755;01/12/2023;JOÃO MARIA CONSTANCIO DE OLIVEIRA;114.128.339-57;0;0;
501;GABRIELA DE MORAES ALVES DA SILVA;106.626.889-45;3755;01/12/2023;;;0;0;
501;GABRIELA DE MORAES ALVES DA SILVA;106.626.889-45;3755;01/12/2023;CAIO MORAIS DE OLIVEIRA;165.723.969-16;0;0;
501;LHAIS VERGILIO DELAVY;084.277.279-09;3755;01/10/2025;;;0;0;
501;LHAIS VERGILIO DELAVY;084.277.279-09;3755;01/10/2025;LELIO ALEXANDRE CANTERO;072.445.239-71;0;0;
501;MAICON LUCAS DOS SANTOS PEIXOTO;087.669.999-95;3755;01/12/2023;LUCA DO PRADO PEIXOTO;000.448.679-05;0;0;
501;MAICON LUCAS DOS SANTOS PEIXOTO;087.669.999-95;3755;01/12/2023;GABRIELA DO PRADO;068.889.729-09;0;0;
501;MAICON LUCAS DOS SANTOS PEIXOTO;087.669.999-95;3755;01/12/2023;;;0;0;
501;MARCIO JOSE TEIXEIRA COSTA;007.193.919-97;3755;01/12/2023;KARINE APARECIDA VIEIRA;050.663.469-88;0;0;
501;MARCIO JOSE TEIXEIRA COSTA;007.193.919-97;3755;01/12/2023;;;0;0;
501;MARCIO JOSE TEIXEIRA COSTA;007.193.919-97;3755;01/12/2023;MARIA LAURA VIEIRA COSTA;144.420.859-43;0;0;
1 CODIGOEMPRESA NOMEFUNC CPFFUNC CODIGOOUTEMP DATAINICIAL NOMEDEPENDENTE CPFDEPENDENTE VALOREMPRESA VALOR DESCRICAO
2 501 ALINE KIEPER 044.273.299-69 3755 01/12/2023 0 0
3 501 ELIZANGELA DE PAULA KUHN 815.257.459-72 3755 01/12/2023 0 0
4 501 ELIZANGELA DE PAULA KUHN 815.257.459-72 3755 01/12/2023 JORGE RICARDO KUHN 522.794.640-04 0 0
5 501 GABRIELA DE MORAES ALVES DA SILVA 106.626.889-45 3755 01/12/2023 JOÃO MARIA CONSTANCIO DE OLIVEIRA 114.128.339-57 0 0
6 501 GABRIELA DE MORAES ALVES DA SILVA 106.626.889-45 3755 01/12/2023 0 0
7 501 GABRIELA DE MORAES ALVES DA SILVA 106.626.889-45 3755 01/12/2023 CAIO MORAIS DE OLIVEIRA 165.723.969-16 0 0
8 501 LHAIS VERGILIO DELAVY 084.277.279-09 3755 01/10/2025 0 0
9 501 LHAIS VERGILIO DELAVY 084.277.279-09 3755 01/10/2025 LELIO ALEXANDRE CANTERO 072.445.239-71 0 0
10 501 MAICON LUCAS DOS SANTOS PEIXOTO 087.669.999-95 3755 01/12/2023 LUCA DO PRADO PEIXOTO 000.448.679-05 0 0
11 501 MAICON LUCAS DOS SANTOS PEIXOTO 087.669.999-95 3755 01/12/2023 GABRIELA DO PRADO 068.889.729-09 0 0
12 501 MAICON LUCAS DOS SANTOS PEIXOTO 087.669.999-95 3755 01/12/2023 0 0
13 501 MARCIO JOSE TEIXEIRA COSTA 007.193.919-97 3755 01/12/2023 KARINE APARECIDA VIEIRA 050.663.469-88 0 0
14 501 MARCIO JOSE TEIXEIRA COSTA 007.193.919-97 3755 01/12/2023 0 0
15 501 MARCIO JOSE TEIXEIRA COSTA 007.193.919-97 3755 01/12/2023 MARIA LAURA VIEIRA COSTA 144.420.859-43 0 0

View File

@ -62,6 +62,7 @@ MODULE_APPS = {
], ],
"utilitarios": [ "utilitarios": [
{"key": "importacao-plano-saude", "label": "Importação de Plano de Saúde"}, {"key": "importacao-plano-saude", "label": "Importação de Plano de Saúde"},
{"key": "importacao-plano-saude-de-paula", "label": "Importação de Plano de Saúde - De Paula"},
], ],
"integracoes": [ "integracoes": [
{"key": "questor", "label": "Questor"}, {"key": "questor", "label": "Questor"},

View File

@ -173,6 +173,20 @@ class Command(BaseCommand):
# tem acesso por padrão, liberado manualmente pra outros perfis depois # tem acesso por padrão, liberado manualmente pra outros perfis depois
# pela tela de Perfis de Acesso. # pela tela de Perfis de Acesso.
permissoes["relatorios"]["apps"]["nao-conformidades"] = False permissoes["relatorios"]["apps"]["nao-conformidades"] = False
# "utilitarios" está em SECTORAL_KEYS — "importacao-plano-saude-de-paula"
# lida com dado pessoal de colaborador do próprio escritório (plano de
# saúde), não dado de cliente como o resto de Utilitários, então nasce
# restrita: só Integração e Inovação tem acesso por padrão, liberado
# manualmente pra outros perfis depois pela tela de Perfis de Acesso —
# mesmo padrão de "Não Conformidades" acima. Decisão explícita do
# usuário: esse é o padrão a seguir daqui pra frente pra qualquer
# aplicação NOVA que lide com dado sensível (nasce restrita a Inovação,
# nunca ampla igual ao resto do módulo/setor) — não retroagir nas 3
# aplicações de toggle único que já existiam antes desta decisão
# (Importação de Plano de Saúde original, Simulação de Custo de
# Contratação, Indicador de Desempenho), que continuam com a
# visibilidade ampla de sempre.
permissoes["utilitarios"]["apps"]["importacao-plano-saude-de-paula"] = False
# `nome` só é gravado na criação — reexecuções não devem reverter uma # `nome` só é gravado na criação — reexecuções não devem reverter uma
# renomeação feita depois pela própria tela de Perfis de Acesso (mesmo # renomeação feita depois pela própria tela de Perfis de Acesso (mesmo
# espírito de "só inicializa na primeira criação" já usado pra # espírito de "só inicializa na primeira criação" já usado pra

View File

@ -0,0 +1,173 @@
# Generated by Django 6.0.7 on 2026-08-28 19:48
import django.db.models.deletion
import django.db.models.functions.text
import portal_api.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('portal_api', '0053_alter_ncocorrencia_assunto'),
]
operations = [
migrations.CreateModel(
name='ImportacaoPlanoSaudeDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('operadora', models.CharField(max_length=50, verbose_name='Operadora')),
('nome_operadora', models.CharField(max_length=50, verbose_name='Nome da operadora')),
('tipos_lancamento', models.JSONField(default=list, verbose_name='Tipos de lançamento')),
('custeio_por_tipo', models.JSONField(default=dict, verbose_name='Custeio por tipo')),
('regra_empresa', models.CharField(blank=True, max_length=50, verbose_name='Regra empresa (mensalidade)')),
('planilha_padrao', models.FileField(blank=True, max_length=255, upload_to='planos_saude_de_paula/planilha_padrao/', validators=[portal_api.models.validar_tamanho_arquivo_plano_saude], verbose_name='Planilha padrão (Questor)')),
('competencia', models.DateField(blank=True, null=True, verbose_name='Competência (Questor)')),
('status', models.CharField(choices=[('revisao', 'Em revisão'), ('concluida', 'Concluída')], default='revisao', max_length=20, verbose_name='Status')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('concluida_em', models.DateTimeField(blank=True, null=True, verbose_name='Concluída em')),
('criado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='importacoes_plano_saude_de_paula', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Importação de plano de saúde - De Paula',
'verbose_name_plural': 'Importações de plano de saúde - De Paula',
'ordering': ['-criado_em'],
},
),
migrations.CreateModel(
name='ImportacaoPlanoSaudeDePaulaArquivoOperadora',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('arquivo', models.FileField(max_length=255, upload_to='planos_saude_de_paula/operadora/', validators=[portal_api.models.validar_tamanho_arquivo_plano_saude], verbose_name='Arquivo')),
('ordem', models.PositiveIntegerField(default=0, verbose_name='Ordem')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('importacao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='arquivos_operadora', to='portal_api.importacaoplanosaudedepaula')),
],
options={
'verbose_name': 'Arquivo da operadora (importação de plano de saúde - De Paula)',
'verbose_name_plural': 'Arquivos da operadora (importação de plano de saúde - De Paula)',
'ordering': ['ordem', 'id'],
},
),
migrations.CreateModel(
name='ImportacaoPlanoSaudeDePaulaLinha',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tipo_lancamento', models.CharField(max_length=20, verbose_name='Tipo de lançamento')),
('codigo_empresa', models.CharField(blank=True, max_length=20, verbose_name='Código empresa')),
('nome_func', models.CharField(blank=True, max_length=150, verbose_name='Nome do funcionário')),
('cpf_func', models.CharField(blank=True, max_length=20, verbose_name='CPF do funcionário')),
('codigo_out_emp', models.CharField(blank=True, max_length=20, verbose_name='Código out. emp.')),
('data_inicial', models.CharField(blank=True, max_length=10, verbose_name='Data inicial')),
('nome_dependente', models.CharField(blank=True, max_length=150, verbose_name='Nome do dependente')),
('cpf_dependente', models.CharField(blank=True, max_length=20, verbose_name='CPF do dependente')),
('valor_empresa', models.CharField(blank=True, default='0', max_length=20, validators=[portal_api.models.validar_valor_monetario_br], verbose_name='Valor empresa')),
('valor', models.CharField(blank=True, default='0', max_length=20, validators=[portal_api.models.validar_valor_monetario_br], verbose_name='Valor')),
('descricao', models.CharField(blank=True, max_length=255, verbose_name='Descrição')),
('ordem', models.PositiveIntegerField(default=0, verbose_name='Ordem')),
('importacao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='linhas', to='portal_api.importacaoplanosaudedepaula')),
],
options={
'verbose_name': 'Linha de importação de plano de saúde - De Paula',
'verbose_name_plural': 'Linhas de importação de plano de saúde - De Paula',
'ordering': ['tipo_lancamento', 'ordem', 'id'],
},
),
migrations.CreateModel(
name='ImportacaoPlanoSaudeDePaulaAuditoria',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('motivo', models.CharField(max_length=30, verbose_name='Motivo')),
('tipo_lancamento', models.CharField(blank=True, max_length=20, verbose_name='Tipo de lançamento')),
('numero_beneficiario', models.CharField(blank=True, max_length=30, verbose_name='Número do beneficiário')),
('nome', models.CharField(blank=True, max_length=150, verbose_name='Nome')),
('cpf', models.CharField(blank=True, max_length=20, verbose_name='CPF')),
('tipo', models.CharField(blank=True, max_length=5, verbose_name='Tipo')),
('valor', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='Valor')),
('detalhe', models.TextField(blank=True, verbose_name='Detalhe/justificativa')),
('resolvida', models.BooleanField(default=False, verbose_name='Resolvida manualmente')),
('importacao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='itens_auditoria', to='portal_api.importacaoplanosaudedepaula')),
('linha_vinculada', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='itens_auditoria_resolvidos', to='portal_api.importacaoplanosaudedepaulalinha')),
],
options={
'verbose_name': 'Item de auditoria de importação de plano de saúde - De Paula',
'verbose_name_plural': 'Itens de auditoria de importação de plano de saúde - De Paula',
'ordering': ['id'],
},
),
migrations.CreateModel(
name='RegraCusteioPlanoSaudeDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nome', models.CharField(max_length=100, verbose_name='Nome')),
('codigo_empresa', models.CharField(max_length=20, verbose_name='Código da empresa')),
('operadora', models.CharField(max_length=50, verbose_name='Operadora')),
('regra_empresa_chave', models.CharField(blank=True, max_length=50, verbose_name='Regra empresa (mensalidade)')),
('tipos_lancamento', models.JSONField(default=list, verbose_name='Tipos de lançamento')),
('custeio_por_tipo', models.JSONField(default=dict, verbose_name='Custeio por tipo')),
('observacoes', models.TextField(blank=True, verbose_name='Observações')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('atualizado_em', models.DateTimeField(auto_now=True)),
('criado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='regras_custeio_plano_saude_de_paula', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Regra de custeio de plano de saúde - De Paula',
'verbose_name_plural': 'Regras de custeio de plano de saúde - De Paula',
'ordering': [django.db.models.functions.text.Length('codigo_empresa'), 'codigo_empresa', 'operadora'],
'unique_together': {('codigo_empresa', 'operadora')},
},
),
migrations.AddField(
model_name='importacaoplanosaudedepaula',
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.regracusteioplanosaudedepaula'),
),
migrations.CreateModel(
name='VinculoNomeOperadoraDePaula',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('operadora', models.CharField(max_length=50, verbose_name='Operadora')),
('codigo_empresa', models.CharField(max_length=20, verbose_name='Código empresa')),
('nome_arquivo_operadora', models.CharField(help_text='Já normalizado (maiúsculas, sem acento) — é a chave de busca do DE/PARA.', max_length=150, verbose_name='Nome divergente (arquivo da operadora)')),
('nome_func_destino', models.CharField(blank=True, max_length=150, verbose_name='Nome do titular vinculado (planilha padrão)')),
('nome_dependente_destino', models.CharField(blank=True, max_length=150, verbose_name='Nome do dependente vinculado (planilha padrão)')),
('criado_em', models.DateTimeField(auto_now_add=True, verbose_name='Criado em')),
('criado_por', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vinculos_nome_plano_saude_de_paula', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Vínculo de nome (plano de saúde - De Paula)',
'verbose_name_plural': 'Vínculos de nome (plano de saúde - De Paula)',
'ordering': ['-criado_em'],
},
),
migrations.CreateModel(
name='ImportacaoPlanoSaudeDePaulaAlteracao',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tipo', models.CharField(choices=[('edicao', 'Edição de valor'), ('inclusao', 'Inclusão de linha'), ('exclusao', 'Exclusão de linha'), ('vinculo_automatico', 'Vínculo automático de nome')], max_length=20, verbose_name='Tipo')),
('tipo_lancamento', models.CharField(blank=True, max_length=20, verbose_name='Tipo de lançamento')),
('campo', models.CharField(blank=True, max_length=30, verbose_name='Campo alterado')),
('valor_anterior', models.TextField(blank=True, verbose_name='Valor anterior')),
('valor_novo', models.TextField(blank=True, verbose_name='Valor novo')),
('dados_linha', models.JSONField(blank=True, default=dict, verbose_name='Dados da linha')),
('criado_em', models.DateTimeField(auto_now_add=True)),
('revertida', models.BooleanField(default=False, verbose_name='Revertida')),
('revertida_em', models.DateTimeField(blank=True, null=True, verbose_name='Revertida em')),
('importacao', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alteracoes', to='portal_api.importacaoplanosaudedepaula')),
('usuario', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alteracoes_plano_saude_de_paula', to=settings.AUTH_USER_MODEL)),
('linha', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alteracoes', to='portal_api.importacaoplanosaudedepaulalinha')),
('vinculo_nome', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alteracoes', to='portal_api.vinculonomeoperadoradepaula')),
],
options={
'verbose_name': 'Alteração de importação de plano de saúde - De Paula',
'verbose_name_plural': 'Alterações de importação de plano de saúde - De Paula',
'ordering': ['-criado_em'],
},
),
migrations.AddConstraint(
model_name='vinculonomeoperadoradepaula',
constraint=models.UniqueConstraint(fields=('operadora', 'codigo_empresa', 'nome_arquivo_operadora'), name='vinculo_nome_operadora_de_paula_unico'),
),
]

View File

@ -959,6 +959,260 @@ class RegraCusteioPlanoSaude(models.Model):
return self.nome return self.nome
class ImportacaoPlanoSaudeDePaula(models.Model):
"""Mesma ferramenta de `ImportacaoPlanoSaude` acima, mas pro plano de
saúde dos próprios colaboradores do escritório De Paula, não de clientes
— ver "Importação de Plano de Saúde - De Paula" no CLAUDE.md do pacote
`portal_api/planos_saude/`. Tabela própria, sem nenhuma FK cruzando com
`ImportacaoPlanoSaude`: o pipeline de extração (`portal_api.planos_saude.*`)
é o mesmo, só o histórico/permissão são independentes."""
STATUS_REVISAO = "revisao"
STATUS_CONCLUIDA = "concluida"
STATUS_CHOICES = [
(STATUS_REVISAO, "Em revisão"),
(STATUS_CONCLUIDA, "Concluída"),
]
operadora = models.CharField("Operadora", max_length=50)
nome_operadora = models.CharField("Nome da operadora", max_length=50)
tipos_lancamento = models.JSONField("Tipos de lançamento", default=list)
custeio_por_tipo = models.JSONField("Custeio por tipo", default=dict)
regra_empresa = models.CharField("Regra empresa (mensalidade)", max_length=50, blank=True)
regra_custeio_salva = models.ForeignKey(
"RegraCusteioPlanoSaudeDePaula",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="importacoes_plano_saude",
)
planilha_padrao = models.FileField(
"Planilha padrão (Questor)",
upload_to="planos_saude_de_paula/planilha_padrao/",
max_length=255,
blank=True,
validators=[validar_tamanho_arquivo_plano_saude],
)
competencia = models.DateField("Competência (Questor)", null=True, blank=True)
status = models.CharField("Status", max_length=20, choices=STATUS_CHOICES, default=STATUS_REVISAO)
criado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, related_name="importacoes_plano_saude_de_paula"
)
criado_em = models.DateTimeField(auto_now_add=True)
concluida_em = models.DateTimeField("Concluída em", null=True, blank=True)
class Meta:
verbose_name = "Importação de plano de saúde - De Paula"
verbose_name_plural = "Importações de plano de saúde - De Paula"
ordering = ["-criado_em"]
def __str__(self) -> str:
return f"{self.nome_operadora} ({self.criado_em:%d/%m/%Y})"
class ImportacaoPlanoSaudeDePaulaArquivoOperadora(models.Model):
"""Ver `ImportacaoPlanoSaudeArquivoOperadora` — mesmo papel, escopo De Paula."""
importacao = models.ForeignKey(
ImportacaoPlanoSaudeDePaula, on_delete=models.CASCADE, related_name="arquivos_operadora"
)
arquivo = models.FileField(
"Arquivo",
upload_to="planos_saude_de_paula/operadora/",
max_length=255,
validators=[validar_tamanho_arquivo_plano_saude],
)
ordem = models.PositiveIntegerField("Ordem", default=0)
criado_em = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Arquivo da operadora (importação de plano de saúde - De Paula)"
verbose_name_plural = "Arquivos da operadora (importação de plano de saúde - De Paula)"
ordering = ["ordem", "id"]
def __str__(self) -> str:
return f"{self.importacao} — {self.arquivo.name}"
class ImportacaoPlanoSaudeDePaulaLinha(models.Model):
"""Ver `ImportacaoPlanoSaudeLinha` — mesmo papel, escopo De Paula."""
importacao = models.ForeignKey(
ImportacaoPlanoSaudeDePaula, on_delete=models.CASCADE, related_name="linhas"
)
tipo_lancamento = models.CharField("Tipo de lançamento", max_length=20)
codigo_empresa = models.CharField("Código empresa", max_length=20, blank=True)
nome_func = models.CharField("Nome do funcionário", max_length=150, blank=True)
cpf_func = models.CharField("CPF do funcionário", max_length=20, blank=True)
codigo_out_emp = models.CharField("Código out. emp.", max_length=20, blank=True)
data_inicial = models.CharField("Data inicial", max_length=10, blank=True)
nome_dependente = models.CharField("Nome do dependente", max_length=150, blank=True)
cpf_dependente = models.CharField("CPF do dependente", max_length=20, blank=True)
valor_empresa = models.CharField(
"Valor empresa", max_length=20, blank=True, default="0", validators=[validar_valor_monetario_br]
)
valor = models.CharField(
"Valor", max_length=20, blank=True, default="0", validators=[validar_valor_monetario_br]
)
descricao = models.CharField("Descrição", max_length=255, blank=True)
ordem = models.PositiveIntegerField("Ordem", default=0)
class Meta:
verbose_name = "Linha de importação de plano de saúde - De Paula"
verbose_name_plural = "Linhas de importação de plano de saúde - De Paula"
ordering = ["tipo_lancamento", "ordem", "id"]
def __str__(self) -> str:
return f"{self.importacao} → {self.nome_func}"
class ImportacaoPlanoSaudeDePaulaAuditoria(models.Model):
"""Ver `ImportacaoPlanoSaudeAuditoria` — mesmo papel, escopo De Paula."""
MOTIVOS_RESOLVIVEIS = ("NOME_DIVERGENTE", "NAO_CADASTRADO")
importacao = models.ForeignKey(
ImportacaoPlanoSaudeDePaula, on_delete=models.CASCADE, related_name="itens_auditoria"
)
motivo = models.CharField("Motivo", max_length=30)
tipo_lancamento = models.CharField("Tipo de lançamento", max_length=20, blank=True)
numero_beneficiario = models.CharField("Número do beneficiário", max_length=30, blank=True)
nome = models.CharField("Nome", max_length=150, blank=True)
cpf = models.CharField("CPF", max_length=20, blank=True)
tipo = models.CharField("Tipo", max_length=5, blank=True)
valor = models.DecimalField("Valor", max_digits=12, decimal_places=2, default=0)
detalhe = models.TextField("Detalhe/justificativa", blank=True)
resolvida = models.BooleanField("Resolvida manualmente", default=False)
linha_vinculada = models.ForeignKey(
ImportacaoPlanoSaudeDePaulaLinha,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="itens_auditoria_resolvidos",
)
class Meta:
verbose_name = "Item de auditoria de importação de plano de saúde - De Paula"
verbose_name_plural = "Itens de auditoria de importação de plano de saúde - De Paula"
ordering = ["id"]
class VinculoNomeOperadoraDePaula(models.Model):
"""Ver `VinculoNomeOperadora` — mesmo "DE/PARA" persistente, escopo De
Paula. Tabela própria pra um vínculo resolvido no escopo cliente nunca
vazar pro escopo De Paula (ou vice-versa) mesmo que operadora+código de
empresa coincidam por acaso."""
operadora = models.CharField("Operadora", max_length=50)
codigo_empresa = models.CharField("Código empresa", max_length=20)
nome_arquivo_operadora = models.CharField(
"Nome divergente (arquivo da operadora)",
max_length=150,
help_text="Já normalizado (maiúsculas, sem acento) — é a chave de busca do DE/PARA.",
)
nome_func_destino = models.CharField("Nome do titular vinculado (planilha padrão)", max_length=150, blank=True)
nome_dependente_destino = models.CharField(
"Nome do dependente vinculado (planilha padrão)", max_length=150, blank=True
)
criado_em = models.DateTimeField("Criado em", auto_now_add=True)
criado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, related_name="vinculos_nome_plano_saude_de_paula"
)
class Meta:
verbose_name = "Vínculo de nome (plano de saúde - De Paula)"
verbose_name_plural = "Vínculos de nome (plano de saúde - De Paula)"
ordering = ["-criado_em"]
constraints = [
models.UniqueConstraint(
fields=["operadora", "codigo_empresa", "nome_arquivo_operadora"],
name="vinculo_nome_operadora_de_paula_unico",
)
]
def __str__(self) -> str:
destino = self.nome_func_destino or self.nome_dependente_destino
return f"{self.nome_arquivo_operadora} → {destino}"
class ImportacaoPlanoSaudeDePaulaAlteracao(models.Model):
"""Ver `ImportacaoPlanoSaudeAlteracao` — mesmo log de edição/inclusão/
exclusão/vínculo automático, escopo De Paula."""
TIPO_EDICAO = "edicao"
TIPO_INCLUSAO = "inclusao"
TIPO_EXCLUSAO = "exclusao"
TIPO_VINCULO_AUTOMATICO = "vinculo_automatico"
TIPO_CHOICES = [
(TIPO_EDICAO, "Edição de valor"),
(TIPO_INCLUSAO, "Inclusão de linha"),
(TIPO_EXCLUSAO, "Exclusão de linha"),
(TIPO_VINCULO_AUTOMATICO, "Vínculo automático de nome"),
]
importacao = models.ForeignKey(
ImportacaoPlanoSaudeDePaula, on_delete=models.CASCADE, related_name="alteracoes"
)
tipo = models.CharField("Tipo", max_length=20, choices=TIPO_CHOICES)
linha = models.ForeignKey(
ImportacaoPlanoSaudeDePaulaLinha,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="alteracoes",
)
tipo_lancamento = models.CharField("Tipo de lançamento", max_length=20, blank=True)
campo = models.CharField("Campo alterado", max_length=30, blank=True)
valor_anterior = models.TextField("Valor anterior", blank=True)
valor_novo = models.TextField("Valor novo", blank=True)
dados_linha = models.JSONField("Dados da linha", default=dict, blank=True)
vinculo_nome = models.ForeignKey(
VinculoNomeOperadoraDePaula, on_delete=models.SET_NULL, null=True, blank=True, related_name="alteracoes"
)
usuario = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, related_name="alteracoes_plano_saude_de_paula"
)
criado_em = models.DateTimeField(auto_now_add=True)
revertida = models.BooleanField("Revertida", default=False)
revertida_em = models.DateTimeField("Revertida em", null=True, blank=True)
class Meta:
verbose_name = "Alteração de importação de plano de saúde - De Paula"
verbose_name_plural = "Alterações de importação de plano de saúde - De Paula"
ordering = ["-criado_em"]
def __str__(self) -> str:
return f"{self.importacao} — {self.get_tipo_display()}"
class RegraCusteioPlanoSaudeDePaula(models.Model):
"""Ver `RegraCusteioPlanoSaude` — mesmo cadastro de regra de custeio por
empresa+operadora, escopo De Paula. Permissão própria
(`PermissaoApp("utilitarios", "importacao-plano-saude-de-paula")`)."""
nome = models.CharField("Nome", max_length=100)
codigo_empresa = models.CharField("Código da empresa", max_length=20)
operadora = models.CharField("Operadora", max_length=50)
regra_empresa_chave = models.CharField("Regra empresa (mensalidade)", max_length=50, blank=True)
tipos_lancamento = models.JSONField("Tipos de lançamento", default=list)
custeio_por_tipo = models.JSONField("Custeio por tipo", default=dict)
observacoes = models.TextField("Observações", blank=True)
criado_por = models.ForeignKey(
Usuario, on_delete=models.SET_NULL, null=True, related_name="regras_custeio_plano_saude_de_paula"
)
criado_em = models.DateTimeField(auto_now_add=True)
atualizado_em = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Regra de custeio de plano de saúde - De Paula"
verbose_name_plural = "Regras de custeio de plano de saúde - De Paula"
unique_together = [["codigo_empresa", "operadora"]]
ordering = [Length("codigo_empresa"), "codigo_empresa", "operadora"]
def __str__(self) -> str:
return self.nome
class IndicadorDepartamento(models.Model): class IndicadorDepartamento(models.Model):
"""Departamento organizacional usado pelo Indicador de Desempenho — cada """Departamento organizacional usado pelo Indicador de Desempenho — cada
um tem seu próprio cadastro de critérios (`IndicadorCriterio`) e um tem seu próprio cadastro de critérios (`IndicadorCriterio`) e

View File

@ -24,6 +24,10 @@ from .models import (
ImportacaoPlanoSaudeAlteracao, ImportacaoPlanoSaudeAlteracao,
ImportacaoPlanoSaudeAuditoria, ImportacaoPlanoSaudeAuditoria,
ImportacaoPlanoSaudeLinha, ImportacaoPlanoSaudeLinha,
ImportacaoPlanoSaudeDePaula,
ImportacaoPlanoSaudeDePaulaAlteracao,
ImportacaoPlanoSaudeDePaulaAuditoria,
ImportacaoPlanoSaudeDePaulaLinha,
IndicadorApuracao, IndicadorApuracao,
IndicadorApuracaoColaborador, IndicadorApuracaoColaborador,
IndicadorApuracaoEmpresa, IndicadorApuracaoEmpresa,
@ -44,6 +48,7 @@ from .models import (
Ramal, Ramal,
RamalAusencia, RamalAusencia,
RegraCusteioPlanoSaude, RegraCusteioPlanoSaude,
RegraCusteioPlanoSaudeDePaula,
TelefoneExterno, TelefoneExterno,
Usuario, Usuario,
WidgetUsuario, WidgetUsuario,
@ -1152,6 +1157,475 @@ class RegraCusteioPlanoSaudeSerializer(serializers.ModelSerializer):
return attrs return attrs
# --- "Importação de Plano de Saúde - De Paula" ---------------------------
# Mesmos serializers acima, apontando pros models "DePaula" (tabelas
# próprias, plano de saúde dos colaboradores do escritório, não de
# clientes — ver CLAUDE.md em portal_api/planos_saude/). Os helpers
# module-level (_monta_regra_custeio, _valor_custeio_para_texto_br,
# _nome_operadora_sem_codigo, _nome_empresa_cacheado,
# _codigo_empresa_da_importacao) são genéricos o bastante (operam por
# duck typing sobre o objeto recebido) pra serem reaproveitados sem
# duplicação.
class ImportacaoPlanoSaudeDePaulaCreateSerializer(serializers.Serializer):
"""Ver `ImportacaoPlanoSaudeCreateSerializer` — mesma validação de
entrada, escopo De Paula."""
operadora = serializers.ChoiceField(choices=list(OPERADORAS.keys()))
planilha_padrao = serializers.FileField(required=False)
competencia = serializers.DateField(required=False, allow_null=True)
arquivo_operadora = serializers.ListField(child=serializers.FileField(), allow_empty=False)
tipos_lancamento = serializers.CharField()
regra_empresa = serializers.CharField(required=False, allow_blank=True)
regra_custeio_salva = serializers.PrimaryKeyRelatedField(
queryset=RegraCusteioPlanoSaudeDePaula.objects.all(), required=False, allow_null=True
)
custeio_mensalidade_titular = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
custeio_mensalidade_dependente = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
custeio_coparticipacao_titular = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
custeio_coparticipacao_dependente = serializers.ChoiceField(choices=CUSTEIOS_VALIDOS, required=False)
limite_valor_mensalidade_titular = serializers.CharField(required=False, allow_blank=True)
percentual_mensalidade_titular = serializers.CharField(required=False, allow_blank=True)
limite_desconto_empregado_mensalidade_titular = serializers.CharField(required=False, allow_blank=True)
limite_valor_mensalidade_dependente = serializers.CharField(required=False, allow_blank=True)
percentual_mensalidade_dependente = serializers.CharField(required=False, allow_blank=True)
limite_desconto_empregado_mensalidade_dependente = serializers.CharField(required=False, allow_blank=True)
limite_valor_coparticipacao_titular = serializers.CharField(required=False, allow_blank=True)
percentual_coparticipacao_titular = serializers.CharField(required=False, allow_blank=True)
limite_desconto_empregado_coparticipacao_titular = serializers.CharField(required=False, allow_blank=True)
limite_valor_coparticipacao_dependente = serializers.CharField(required=False, allow_blank=True)
percentual_coparticipacao_dependente = serializers.CharField(required=False, allow_blank=True)
limite_desconto_empregado_coparticipacao_dependente = serializers.CharField(required=False, allow_blank=True)
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
tipos = [t.strip() for t in attrs["tipos_lancamento"].split(",") if t.strip()]
if not tipos:
raise serializers.ValidationError({"tipos_lancamento": "Selecione ao menos um tipo de importação."})
regra_empresa_key = (attrs.get("regra_empresa") or "").strip()
regra_empresa_tipos: tuple[str, ...] = ()
if regra_empresa_key:
regra_re = REGRAS_EMPRESA.get(regra_empresa_key)
if regra_re is None:
raise serializers.ValidationError({"regra_empresa": f"Regra empresa desconhecida: {regra_empresa_key}"})
regra_empresa_tipos = tuple(t for t in regra_re.get("tipos_lancamento", ("mensalidade",)) if t in tipos)
if not regra_empresa_tipos:
raise serializers.ValidationError(
{
"regra_empresa": (
f"\"{regra_re['label']}\" cobre {'/'.join(regra_re.get('tipos_lancamento', ()))}, "
"mas nenhum desses tipos de importação está selecionado."
)
}
)
parser_instancia = OPERADORAS[attrs["operadora"]]["parser"]()
casamento_exigido = regra_re.get("chave_casamento", "nome")
for tipo in regra_empresa_tipos:
if parser_instancia.chave_casamento_para_tipo(tipo) != casamento_exigido:
raise serializers.ValidationError(
{
"regra_empresa": (
f"Esta operadora não é compatível com \"{regra_re['label']}\" "
f"(exige casamento por {casamento_exigido!r})."
)
}
)
custeio_por_tipo: dict[str, Any] = {}
for tipo in tipos:
if tipo not in TIPOS_LANCAMENTO_VALIDOS:
raise serializers.ValidationError({"tipos_lancamento": f"Tipo de importação inválido: {tipo}"})
if tipo in regra_empresa_tipos:
custeio_por_tipo[tipo] = {}
continue
custeio_por_pessoa: dict[str, Any] = {}
for pessoa in TIPOS_PESSOA_VALIDOS:
chave = f"{tipo}_{pessoa}"
custeio_por_pessoa[pessoa] = _monta_regra_custeio(
chave,
attrs.get(f"custeio_{chave}"),
attrs.get(f"limite_valor_{chave}", ""),
attrs.get(f"percentual_{chave}", ""),
attrs.get(f"limite_desconto_empregado_{chave}", ""),
)
custeio_por_tipo[tipo] = custeio_por_pessoa
attrs["tipos_lancamento_lista"] = tipos
attrs["custeio_por_tipo"] = custeio_por_tipo
attrs["regra_empresa"] = regra_empresa_key
planilha_padrao = attrs.get("planilha_padrao")
competencia = attrs.get("competencia")
if planilha_padrao and competencia:
raise serializers.ValidationError(
{"competencia": "Anexe a planilha manualmente ou informe a competência para buscar no Questor — não os dois."}
)
if not planilha_padrao and not competencia:
raise serializers.ValidationError(
{"planilha_padrao": "Anexe a planilha padrão ou informe a competência para buscar no Questor."}
)
if competencia:
regra_custeio_salva = attrs.get("regra_custeio_salva")
if not regra_custeio_salva or not regra_custeio_salva.codigo_empresa:
raise serializers.ValidationError(
{"competencia": "Buscar a planilha no Questor exige selecionar a empresa (regra de custeio cadastrada)."}
)
return attrs
class ImportacaoPlanoSaudeDePaulaLinhaSerializer(serializers.ModelSerializer):
class Meta:
model = ImportacaoPlanoSaudeDePaulaLinha
fields = [
"id",
"tipo_lancamento",
"codigo_empresa",
"nome_func",
"cpf_func",
"codigo_out_emp",
"data_inicial",
"nome_dependente",
"cpf_dependente",
"valor_empresa",
"valor",
"descricao",
]
read_only_fields = ["id", "tipo_lancamento"]
class ImportacaoPlanoSaudeDePaulaLinhaCreateSerializer(serializers.ModelSerializer):
class Meta:
model = ImportacaoPlanoSaudeDePaulaLinha
fields = [
"id",
"importacao",
"tipo_lancamento",
"codigo_empresa",
"nome_func",
"cpf_func",
"codigo_out_emp",
"data_inicial",
"nome_dependente",
"cpf_dependente",
"valor_empresa",
"valor",
"descricao",
]
read_only_fields = ["id"]
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
importacao = attrs.get("importacao")
tipo = attrs.get("tipo_lancamento")
if tipo not in TIPOS_LANCAMENTO_VALIDOS:
raise serializers.ValidationError({"tipo_lancamento": f"Tipo de importação inválido: {tipo}"})
if importacao is not None and tipo not in (importacao.tipos_lancamento or []):
raise serializers.ValidationError(
{"tipo_lancamento": "Esse tipo de lançamento não faz parte desta importação."}
)
return attrs
class ImportacaoPlanoSaudeDePaulaAuditoriaSerializer(serializers.ModelSerializer):
linha_vinculada_nome = serializers.SerializerMethodField()
class Meta:
model = ImportacaoPlanoSaudeDePaulaAuditoria
fields = [
"id",
"motivo",
"tipo_lancamento",
"numero_beneficiario",
"nome",
"cpf",
"tipo",
"valor",
"detalhe",
"resolvida",
"linha_vinculada",
"linha_vinculada_nome",
]
read_only_fields = ["resolvida", "linha_vinculada"]
def get_linha_vinculada_nome(self, obj: ImportacaoPlanoSaudeDePaulaAuditoria) -> str | None:
if not obj.linha_vinculada:
return None
return obj.linha_vinculada.nome_dependente or obj.linha_vinculada.nome_func
class ImportacaoPlanoSaudeDePaulaAlteracaoSerializer(serializers.ModelSerializer):
usuario_nome = serializers.CharField(source="usuario.nome", read_only=True, default=None)
linha_nome = serializers.SerializerMethodField()
vinculo_nome_destino = serializers.SerializerMethodField()
class Meta:
model = ImportacaoPlanoSaudeDePaulaAlteracao
fields = [
"id",
"tipo",
"linha",
"linha_nome",
"tipo_lancamento",
"campo",
"valor_anterior",
"valor_novo",
"dados_linha",
"vinculo_nome",
"vinculo_nome_destino",
"usuario_nome",
"criado_em",
"revertida",
"revertida_em",
]
read_only_fields = fields
def get_linha_nome(self, obj: ImportacaoPlanoSaudeDePaulaAlteracao) -> str | None:
dados = obj.dados_linha or {}
if obj.linha_id:
return obj.linha.nome_dependente or obj.linha.nome_func or None
return dados.get("nome_dependente") or dados.get("nome_func") or None
def get_vinculo_nome_destino(self, obj: ImportacaoPlanoSaudeDePaulaAlteracao) -> str | None:
if not obj.vinculo_nome_id:
return None
vinculo = obj.vinculo_nome
return vinculo.nome_func_destino or vinculo.nome_dependente_destino or None
class ImportacaoPlanoSaudeDePaulaListSerializer(serializers.ModelSerializer):
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
codigo_empresa = serializers.SerializerMethodField()
class Meta:
model = ImportacaoPlanoSaudeDePaula
fields = [
"id",
"nome_operadora",
"codigo_empresa",
"competencia",
"tipos_lancamento",
"custeio_por_tipo",
"status",
"criado_por_nome",
"criado_em",
"concluida_em",
]
def get_codigo_empresa(self, obj: ImportacaoPlanoSaudeDePaula) -> str:
return _codigo_empresa_da_importacao(obj)
class ImportacaoPlanoSaudeDePaulaDetailSerializer(serializers.ModelSerializer):
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
linhas = ImportacaoPlanoSaudeDePaulaLinhaSerializer(many=True, read_only=True)
itens_auditoria = ImportacaoPlanoSaudeDePaulaAuditoriaSerializer(many=True, read_only=True)
alteracoes = ImportacaoPlanoSaudeDePaulaAlteracaoSerializer(many=True, read_only=True)
resumo_por_tipo = serializers.SerializerMethodField()
codigo_empresa = serializers.SerializerMethodField()
nome_empresa = serializers.SerializerMethodField()
regra_empresa_label = serializers.SerializerMethodField()
regra_empresa_observacoes = serializers.SerializerMethodField()
regra_custeio_salva_nome = serializers.CharField(source="regra_custeio_salva.nome", read_only=True, default=None)
regra_custeio_salva_observacoes = serializers.CharField(
source="regra_custeio_salva.observacoes", read_only=True, default=None
)
class Meta:
model = ImportacaoPlanoSaudeDePaula
fields = [
"id",
"operadora",
"nome_operadora",
"codigo_empresa",
"nome_empresa",
"competencia",
"tipos_lancamento",
"custeio_por_tipo",
"regra_empresa",
"regra_empresa_label",
"regra_empresa_observacoes",
"regra_custeio_salva",
"regra_custeio_salva_nome",
"regra_custeio_salva_observacoes",
"status",
"criado_por_nome",
"criado_em",
"concluida_em",
"linhas",
"itens_auditoria",
"alteracoes",
"resumo_por_tipo",
]
def get_codigo_empresa(self, obj: ImportacaoPlanoSaudeDePaula) -> str:
return _codigo_empresa_da_importacao(obj)
def get_nome_empresa(self, obj: ImportacaoPlanoSaudeDePaula) -> str | None:
return _nome_empresa_cacheado(self.get_codigo_empresa(obj))
def get_regra_empresa_label(self, obj: ImportacaoPlanoSaudeDePaula) -> str | None:
regra = REGRAS_EMPRESA.get(obj.regra_empresa)
return regra["label"] if regra else None
def get_regra_empresa_observacoes(self, obj: ImportacaoPlanoSaudeDePaula) -> str | None:
regra = REGRAS_EMPRESA.get(obj.regra_empresa)
observacoes = (regra or {}).get("observacoes")
return observacoes or None
def get_resumo_por_tipo(self, obj: ImportacaoPlanoSaudeDePaula) -> list[dict[str, Any]]:
resumo = []
for tipo in obj.tipos_lancamento:
linhas_do_tipo = [l for l in obj.linhas.all() if l.tipo_lancamento == tipo]
lancadas = sum(1 for l in linhas_do_tipo if l.valor != "0" or l.valor_empresa != "0")
auditoria_do_tipo = sum(
1 for item in obj.itens_auditoria.all() if item.tipo_lancamento == tipo and not item.resolvida
)
resumo.append({
"tipo_lancamento": tipo,
"total_linhas": len(linhas_do_tipo),
"total_lancadas": lancadas,
"total_auditoria": auditoria_do_tipo,
})
return resumo
class RegraCusteioPlanoSaudeDePaulaSerializer(serializers.ModelSerializer):
"""Ver `RegraCusteioPlanoSaudeSerializer` — mesma validação, escopo De
Paula."""
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)
nome = serializers.CharField(read_only=True)
nome_empresa = serializers.SerializerMethodField()
class Meta:
model = RegraCusteioPlanoSaudeDePaula
fields = [
"id",
"nome",
"codigo_empresa",
"nome_empresa",
"operadora",
"regra_empresa_chave",
"tipos_lancamento",
"custeio_por_tipo",
"observacoes",
"criado_por_nome",
"criado_em",
"atualizado_em",
]
read_only_fields = ["id", "criado_por_nome", "criado_em", "atualizado_em"]
validators = [
UniqueTogetherValidator(
queryset=RegraCusteioPlanoSaudeDePaula.objects.all(),
fields=["codigo_empresa", "operadora"],
message="Já existe uma regra de custeio cadastrada para essa empresa com essa operadora.",
)
]
def get_nome_empresa(self, obj: RegraCusteioPlanoSaudeDePaula) -> str | None:
return resolve_nome_empresa(obj.codigo_empresa)
def validate_codigo_empresa(self, value: str) -> str:
value = normalizar_codigo_empresa(value)
if not value:
raise serializers.ValidationError("Informe o código da empresa.")
return value
def validate_operadora(self, value: str) -> str:
value = (value or "").strip()
if value not in OPERADORAS:
raise serializers.ValidationError(f"Operadora inválida: {value}")
return value
def validate_regra_empresa_chave(self, value: str) -> str:
value = (value or "").strip()
if value and value not in REGRAS_EMPRESA:
raise serializers.ValidationError(f"Regra empresa desconhecida: {value}")
return value
def validate_tipos_lancamento(self, value: Any) -> list[str]:
if not isinstance(value, list) or not value:
raise serializers.ValidationError("Selecione ao menos um tipo de importação.")
for tipo in value:
if tipo not in TIPOS_LANCAMENTO_VALIDOS:
raise serializers.ValidationError(f"Tipo de importação inválido: {tipo}")
return value
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
tipos = attrs.get("tipos_lancamento")
if tipos is None:
tipos = self.instance.tipos_lancamento if self.instance else []
codigo_empresa = attrs.get("codigo_empresa", self.instance.codigo_empresa if self.instance else None)
operadora_key = attrs.get("operadora", self.instance.operadora if self.instance else None)
regra_empresa_chave = attrs.get(
"regra_empresa_chave", self.instance.regra_empresa_chave if self.instance else ""
)
custeio_bruto = attrs.get("custeio_por_tipo")
if custeio_bruto is None:
custeio_bruto = self.instance.custeio_por_tipo if self.instance else {}
if not isinstance(custeio_bruto, dict):
raise serializers.ValidationError({"custeio_por_tipo": "Formato inválido."})
regra_empresa_tipos: tuple[str, ...] = ()
if regra_empresa_chave:
regra_re = REGRAS_EMPRESA[regra_empresa_chave]
regra_empresa_tipos = tuple(t for t in regra_re.get("tipos_lancamento", ("mensalidade",)) if t in tipos)
if not regra_empresa_tipos:
raise serializers.ValidationError(
{
"regra_empresa_chave": (
f"\"{regra_re['label']}\" cobre {'/'.join(regra_re.get('tipos_lancamento', ()))}, "
"mas nenhum desses tipos de importação está selecionado."
)
}
)
if regra_re["codigo_empresa"] != codigo_empresa:
raise serializers.ValidationError(
{
"regra_empresa_chave": (
f"Esta regra especial foi cadastrada para a empresa código "
f"{regra_re['codigo_empresa']}, não para {codigo_empresa}."
)
}
)
if regra_re["operadora"] != operadora_key:
raise serializers.ValidationError(
{"regra_empresa_chave": "Esta regra especial foi cadastrada para outra operadora."}
)
custeio_validado: dict[str, Any] = {}
for tipo in tipos:
if tipo in regra_empresa_tipos:
custeio_validado[tipo] = {}
continue
por_tipo = custeio_bruto.get(tipo) or {}
if not isinstance(por_tipo, dict):
raise serializers.ValidationError({"custeio_por_tipo": f"Formato inválido para o tipo {tipo}."})
custeio_por_pessoa: dict[str, Any] = {}
for pessoa in TIPOS_PESSOA_VALIDOS:
chave = f"{tipo}_{pessoa}"
entrada = por_tipo.get(pessoa) or {}
if not isinstance(entrada, dict):
entrada = {}
custeio_por_pessoa[pessoa] = _monta_regra_custeio(
chave,
entrada.get("modo"),
_valor_custeio_para_texto_br(entrada.get("limite_valor")),
_valor_custeio_para_texto_br(entrada.get("percentual")),
_valor_custeio_para_texto_br(entrada.get("limite_desconto_empregado")),
)
custeio_validado[tipo] = custeio_por_pessoa
attrs["custeio_por_tipo"] = custeio_validado
attrs["nome"] = f"{codigo_empresa} - {_nome_operadora_sem_codigo(operadora_key)}"
return attrs
class IndicadorPercentualTipoSerializer(serializers.ModelSerializer): class IndicadorPercentualTipoSerializer(serializers.ModelSerializer):
tipo_label = serializers.CharField(source="get_tipo_display", read_only=True) tipo_label = serializers.CharField(source="get_tipo_display", read_only=True)
criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None) criado_por_nome = serializers.CharField(source="criado_por.nome", read_only=True, default=None)

View File

@ -39,6 +39,31 @@ router.register(
router.register( router.register(
"regras-custeio-plano-saude", views.RegraCusteioPlanoSaudeViewSet, basename="regra-custeio-plano-saude" "regras-custeio-plano-saude", views.RegraCusteioPlanoSaudeViewSet, basename="regra-custeio-plano-saude"
) )
router.register(
"importacoes-plano-saude-de-paula",
views.ImportacaoPlanoSaudeDePaulaViewSet,
basename="importacao-plano-saude-de-paula",
)
router.register(
"importacoes-plano-saude-de-paula-linhas",
views.ImportacaoPlanoSaudeDePaulaLinhaViewSet,
basename="importacao-plano-saude-de-paula-linha",
)
router.register(
"importacoes-plano-saude-de-paula-auditoria",
views.ImportacaoPlanoSaudeDePaulaAuditoriaViewSet,
basename="importacao-plano-saude-de-paula-auditoria",
)
router.register(
"importacoes-plano-saude-de-paula-alteracoes",
views.ImportacaoPlanoSaudeDePaulaAlteracaoViewSet,
basename="importacao-plano-saude-de-paula-alteracao",
)
router.register(
"regras-custeio-plano-saude-de-paula",
views.RegraCusteioPlanoSaudeDePaulaViewSet,
basename="regra-custeio-plano-saude-de-paula",
)
router.register( router.register(
"indicadores-percentuais-tipo", views.IndicadorPercentualTipoViewSet, basename="indicador-percentual-tipo" "indicadores-percentuais-tipo", views.IndicadorPercentualTipoViewSet, basename="indicador-percentual-tipo"
) )

View File

@ -46,6 +46,11 @@ from .models import (
ImportacaoPlanoSaudeArquivoOperadora, ImportacaoPlanoSaudeArquivoOperadora,
ImportacaoPlanoSaudeAuditoria, ImportacaoPlanoSaudeAuditoria,
ImportacaoPlanoSaudeLinha, ImportacaoPlanoSaudeLinha,
ImportacaoPlanoSaudeDePaula,
ImportacaoPlanoSaudeDePaulaAlteracao,
ImportacaoPlanoSaudeDePaulaArquivoOperadora,
ImportacaoPlanoSaudeDePaulaAuditoria,
ImportacaoPlanoSaudeDePaulaLinha,
IndicadorApuracao, IndicadorApuracao,
IndicadorApuracaoColaborador, IndicadorApuracaoColaborador,
IndicadorApuracaoEmpresa, IndicadorApuracaoEmpresa,
@ -66,9 +71,11 @@ from .models import (
Ramal, Ramal,
RamalAusencia, RamalAusencia,
RegraCusteioPlanoSaude, RegraCusteioPlanoSaude,
RegraCusteioPlanoSaudeDePaula,
TelefoneExterno, TelefoneExterno,
Usuario, Usuario,
VinculoNomeOperadora, VinculoNomeOperadora,
VinculoNomeOperadoraDePaula,
WidgetUsuario, WidgetUsuario,
) )
from .indicadores import calculo as indicadores_calculo from .indicadores import calculo as indicadores_calculo
@ -106,6 +113,13 @@ from .serializers import (
ImportacaoPlanoSaudeLinhaCreateSerializer, ImportacaoPlanoSaudeLinhaCreateSerializer,
ImportacaoPlanoSaudeLinhaSerializer, ImportacaoPlanoSaudeLinhaSerializer,
ImportacaoPlanoSaudeListSerializer, ImportacaoPlanoSaudeListSerializer,
ImportacaoPlanoSaudeDePaulaAlteracaoSerializer,
ImportacaoPlanoSaudeDePaulaAuditoriaSerializer,
ImportacaoPlanoSaudeDePaulaCreateSerializer,
ImportacaoPlanoSaudeDePaulaDetailSerializer,
ImportacaoPlanoSaudeDePaulaLinhaCreateSerializer,
ImportacaoPlanoSaudeDePaulaLinhaSerializer,
ImportacaoPlanoSaudeDePaulaListSerializer,
IndicadorApuracaoAjusteDepartamentoSerializer, IndicadorApuracaoAjusteDepartamentoSerializer,
IndicadorApuracaoAjusteGrupoSerializer, IndicadorApuracaoAjusteGrupoSerializer,
IndicadorApuracaoAjusteHonorarioEmpresaSerializer, IndicadorApuracaoAjusteHonorarioEmpresaSerializer,
@ -141,6 +155,7 @@ from .serializers import (
RamalAusenciaSerializer, RamalAusenciaSerializer,
RamalSerializer, RamalSerializer,
RegraCusteioPlanoSaudeSerializer, RegraCusteioPlanoSaudeSerializer,
RegraCusteioPlanoSaudeDePaulaSerializer,
SimulacaoCustoContratacaoEntradaSerializer, SimulacaoCustoContratacaoEntradaSerializer,
TelefoneExternoSerializer, TelefoneExternoSerializer,
UsuarioListSerializer, UsuarioListSerializer,
@ -1496,6 +1511,543 @@ class RegraCusteioPlanoSaudeViewSet(viewsets.ModelViewSet):
return Response({"codigo_empresa": codigo, "nome_empresa": resolve_nome_empresa(codigo)}) return Response({"codigo_empresa": codigo, "nome_empresa": resolve_nome_empresa(codigo)})
# --- "Importação de Plano de Saúde - De Paula" ----------------------------
# Mesmas views acima, sobre os models "DePaula" (tabelas próprias, plano de
# saúde dos colaboradores do escritório, não de clientes — ver CLAUDE.md em
# portal_api/planos_saude/), com permissão independente
# (PermissaoApp("utilitarios", "importacao-plano-saude-de-paula")). O
# pipeline de extração (planos_saude_pipeline/matcher/regras_empresa) e os
# helpers puros (_salva_arquivo_temporario, _valida_planilha_padrao,
# _valida_arquivo_operadora, _monta_csv_linhas_plano_saude,
# _snapshot_linha_plano_saude) são reaproveitados sem duplicação.
def _garante_importacao_de_paula_em_revisao(importacao: ImportacaoPlanoSaudeDePaula) -> None:
"""Ver `_garante_importacao_em_revisao` — mesma trava, escopo De Paula."""
if importacao.status == ImportacaoPlanoSaudeDePaula.STATUS_CONCLUIDA:
raise ValidationError(
{"detail": 'Esta importação já foi concluída. Clique em "Editar" para reabri-la antes de alterar.'}
)
def _carrega_vinculos_por_nome_de_paula(
operadora_key: str, linhas_sistema_template: list[LinhaSistema]
) -> dict[str, VinculoNomePuro]:
"""Ver `_carrega_vinculos_por_nome` — mesmo DE/PARA, lendo de
`VinculoNomeOperadoraDePaula` (tabela própria, nunca cruza com o DE/PARA
da aplicação original)."""
codigos = {
normalizar_codigo_empresa(linha.codigo_empresa)
for linha in linhas_sistema_template
if linha.codigo_empresa
}
if not codigos:
return {}
vinculos = VinculoNomeOperadoraDePaula.objects.filter(operadora=operadora_key)
return {
vinculo.nome_arquivo_operadora: VinculoNomePuro(
id=vinculo.id,
nome_func_destino=vinculo.nome_func_destino,
nome_dependente_destino=vinculo.nome_dependente_destino,
)
for vinculo in vinculos
if normalizar_codigo_empresa(vinculo.codigo_empresa) in codigos
}
class ImportacaoPlanoSaudeDePaulaViewSet(viewsets.ModelViewSet):
"""Ver `ImportacaoPlanoSaudeViewSet` — mesmo fluxo, escopo De Paula."""
http_method_names = ["get", "post", "delete", "head", "options"]
queryset = ImportacaoPlanoSaudeDePaula.objects.all().order_by("-criado_em")
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("utilitarios", "importacao-plano-saude-de-paula")]
def perform_destroy(self, instance: ImportacaoPlanoSaudeDePaula) -> None:
for arquivo in instance.arquivos_operadora.all():
arquivo.arquivo.delete(save=False)
instance.planilha_padrao.delete(save=False)
instance.delete()
def get_serializer_class(self) -> type[ModelSerializer]:
if self.action == "list":
return ImportacaoPlanoSaudeDePaulaListSerializer
return ImportacaoPlanoSaudeDePaulaDetailSerializer
@action(detail=False, methods=["get"])
def operadoras(self, request: Request) -> Response:
return Response(planos_saude_pipeline.lista_operadoras())
@action(detail=False, methods=["get"], url_path="regras-empresa")
def regras_empresa(self, request: Request) -> Response:
return Response(planos_saude_regras_empresa.lista_regras_empresa())
@action(detail=False, methods=["post"], url_path="validar-arquivo")
def validar_arquivo(self, request: Request) -> Response:
arquivo = request.FILES.get("arquivo")
if not arquivo:
raise ValidationError({"arquivo": "Envie um arquivo."})
tipo = request.data.get("tipo")
if tipo == "planilha":
return Response(_valida_planilha_padrao(arquivo))
if tipo == "operadora":
operadora_key = request.data.get("operadora")
if operadora_key not in planos_saude_pipeline.OPERADORAS:
raise ValidationError({"operadora": "Selecione a operadora antes de anexar o arquivo."})
return Response(_valida_arquivo_operadora(arquivo, operadora_key))
raise ValidationError({"tipo": "Informe 'planilha' ou 'operadora'."})
def create(self, request: Request, *args: Any, **kwargs: Any) -> Response:
entrada = ImportacaoPlanoSaudeDePaulaCreateSerializer(data=request.data)
entrada.is_valid(raise_exception=True)
dados = entrada.validated_data
tipos = dados["tipos_lancamento_lista"]
custeio_por_tipo = dados["custeio_por_tipo"]
operadora_key = dados["operadora"]
operadora_info = planos_saude_pipeline.OPERADORAS[operadora_key]
regra_custeio_obj = dados.get("regra_custeio_salva")
competencia = dados.get("competencia")
planilha_padrao_arquivo = dados.get("planilha_padrao")
linhas_sistema_template = None
planilha_padrao_gerada = None
if not planilha_padrao_arquivo:
codigo_empresa_regra = normalizar_codigo_empresa(regra_custeio_obj.codigo_empresa)
codigo_operadora = operadora_info["codigo_operadora"]
try:
linhas_sistema_template = busca_linhas_questor(codigo_empresa_regra, codigo_operadora, competencia)
except Exception:
return Response(
{
"detail": (
"Não foi possível buscar a planilha padrão no Questor agora. "
"Tente novamente em alguns instantes ou anexe a planilha manualmente."
)
},
status=status.HTTP_400_BAD_REQUEST,
)
if not linhas_sistema_template:
return Response(
{
"detail": (
f"Nenhum beneficiário encontrado no Questor para a empresa {regra_custeio_obj.codigo_empresa} "
f"com a operadora {operadora_info['nome']} na competência informada."
)
},
status=status.HTTP_400_BAD_REQUEST,
)
planilha_padrao_gerada = ContentFile(
linhas_para_csv_bytes(linhas_sistema_template),
name=f"questor_{codigo_empresa_regra}_{competencia:%Y-%m}.csv",
)
importacao = ImportacaoPlanoSaudeDePaula.objects.create(
operadora=operadora_key,
nome_operadora=planos_saude_pipeline.label_operadora(operadora_key),
tipos_lancamento=tipos,
custeio_por_tipo=custeio_por_tipo,
regra_empresa=dados["regra_empresa"],
regra_custeio_salva=regra_custeio_obj,
criado_por=request.user,
competencia=competencia,
planilha_padrao=planilha_padrao_arquivo or planilha_padrao_gerada,
)
arquivos_operadora = [
ImportacaoPlanoSaudeDePaulaArquivoOperadora.objects.create(
importacao=importacao, arquivo=arquivo, ordem=ordem
)
for ordem, arquivo in enumerate(dados["arquivo_operadora"])
]
def _limpa_arquivos_orfaos() -> None:
for arquivo in arquivos_operadora:
arquivo.arquivo.delete(save=False)
importacao.planilha_padrao.delete(save=False)
importacao.delete()
try:
if planilha_padrao_arquivo:
linhas_sistema_template = le_planilha_padrao(importacao.planilha_padrao.path)
vinculos_por_nome = _carrega_vinculos_por_nome_de_paula(operadora_key, linhas_sistema_template)
resultado = planos_saude_pipeline.processa_importacao(
operadora_key=operadora_key,
caminhos_arquivo_operadora=[arquivo.arquivo.path for arquivo in arquivos_operadora],
linhas_sistema_template=linhas_sistema_template,
tipos_selecionados=tipos,
custeio_por_tipo=custeio_por_tipo,
regra_empresa_key=dados["regra_empresa"] or None,
vinculos_por_nome=vinculos_por_nome,
)
except RegraEmpresaIncompativelError as exc:
_limpa_arquivos_orfaos()
return Response({"detail": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
except Exception:
_limpa_arquivos_orfaos()
return Response(
{
"detail": (
"O formato de um dos arquivos não está conforme o esperado. "
"Contate a Inovação."
)
},
status=status.HTTP_400_BAD_REQUEST,
)
if planilha_padrao_arquivo and regra_custeio_obj and regra_custeio_obj.codigo_empresa:
todas_linhas = [linha for linhas in resultado.linhas_por_tipo.values() for linha in linhas]
codigo_esperado = normalizar_codigo_empresa(regra_custeio_obj.codigo_empresa)
if not any(normalizar_codigo_empresa(linha.codigo_empresa) == codigo_esperado for linha in todas_linhas):
_limpa_arquivos_orfaos()
return Response(
{
"detail": (
f'A regra "{regra_custeio_obj.nome}" foi cadastrada para a empresa código '
f"{regra_custeio_obj.codigo_empresa}, mas a planilha padrão anexada não tem "
"nenhuma linha com esse código."
)
},
status=status.HTTP_400_BAD_REQUEST,
)
linhas_specs = [
(tipo, ordem, linha)
for tipo, linhas in resultado.linhas_por_tipo.items()
for ordem, linha in enumerate(linhas)
]
linhas_bulk = ImportacaoPlanoSaudeDePaulaLinha.objects.bulk_create([
ImportacaoPlanoSaudeDePaulaLinha(
importacao=importacao,
tipo_lancamento=tipo,
ordem=ordem,
codigo_empresa=linha.codigo_empresa,
nome_func=linha.nome_func,
cpf_func=linha.cpf_func,
codigo_out_emp=linha.codigo_out_emp,
data_inicial=linha.data_inicial,
nome_dependente=linha.nome_dependente,
cpf_dependente=linha.cpf_dependente,
valor_empresa=linha.valor_empresa,
valor=linha.valor,
descricao=linha.descricao,
)
for tipo, ordem, linha in linhas_specs
])
linha_por_tipo_ordem = {
(tipo, ordem): linha_obj for (tipo, ordem, _), linha_obj in zip(linhas_specs, linhas_bulk)
}
alteracoes_vinculo_bulk = []
for vinculo_aplicado in resultado.vinculos_aplicados:
linha_obj = linha_por_tipo_ordem.get((vinculo_aplicado.tipo_lancamento, vinculo_aplicado.indice_linha))
if linha_obj is None:
continue
alteracoes_vinculo_bulk.append(ImportacaoPlanoSaudeDePaulaAlteracao(
importacao=importacao,
tipo=ImportacaoPlanoSaudeDePaulaAlteracao.TIPO_VINCULO_AUTOMATICO,
linha=linha_obj,
tipo_lancamento=vinculo_aplicado.tipo_lancamento,
valor_novo=vinculo_aplicado.nome_arquivo_operadora,
dados_linha=_snapshot_linha_plano_saude(linha_obj),
vinculo_nome_id=vinculo_aplicado.vinculo_id,
usuario=request.user,
))
ImportacaoPlanoSaudeDePaulaAlteracao.objects.bulk_create(alteracoes_vinculo_bulk)
auditoria_bulk = [
ImportacaoPlanoSaudeDePaulaAuditoria(
importacao=importacao,
motivo=item.motivo,
tipo_lancamento=item.tipo_lancamento,
numero_beneficiario=item.numero_beneficiario,
nome=item.nome,
cpf=item.cpf,
tipo=item.tipo,
valor=item.valor,
detalhe=item.detalhe,
)
for item in resultado.auditoria
]
ImportacaoPlanoSaudeDePaulaAuditoria.objects.bulk_create(auditoria_bulk)
serializer = ImportacaoPlanoSaudeDePaulaDetailSerializer(importacao)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=["post"])
def gerar(self, request: Request, pk: str | None = None) -> HttpResponse:
importacao = self.get_object()
linhas_por_tipo: dict[str, list[ImportacaoPlanoSaudeDePaulaLinha]] = {}
for linha in importacao.linhas.order_by("tipo_lancamento", "ordem", "id"):
linhas_por_tipo.setdefault(linha.tipo_lancamento, []).append(linha)
arquivos = {tipo: _monta_csv_linhas_plano_saude(linhas) for tipo, linhas in linhas_por_tipo.items()}
importacao.status = ImportacaoPlanoSaudeDePaula.STATUS_CONCLUIDA
importacao.concluida_em = timezone.now()
importacao.save(update_fields=["status", "concluida_em"])
if len(arquivos) == 1:
(tipo, conteudo), = arquivos.items()
response = HttpResponse(conteudo, content_type="text/csv; charset=utf-8-sig")
response["Content-Disposition"] = f'attachment; filename="{tipo}.csv"'
return response
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w") as zf:
for tipo, conteudo in arquivos.items():
zf.writestr(f"{tipo}.csv", conteudo)
response = HttpResponse(buffer.getvalue(), content_type="application/zip")
response["Content-Disposition"] = f'attachment; filename="importacao_plano_saude_de_paula_{importacao.id}.zip"'
return response
@action(detail=True, methods=["post"])
def reabrir(self, request: Request, pk: str | None = None) -> Response:
importacao = self.get_object()
importacao.status = ImportacaoPlanoSaudeDePaula.STATUS_REVISAO
importacao.concluida_em = None
importacao.save(update_fields=["status", "concluida_em"])
serializer = ImportacaoPlanoSaudeDePaulaDetailSerializer(importacao)
return Response(serializer.data)
class ImportacaoPlanoSaudeDePaulaLinhaViewSet(viewsets.ModelViewSet):
"""Ver `ImportacaoPlanoSaudeLinhaViewSet` — mesmo fluxo, escopo De Paula."""
http_method_names = ["get", "post", "patch", "delete", "head", "options"]
queryset = ImportacaoPlanoSaudeDePaulaLinha.objects.all()
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("utilitarios", "importacao-plano-saude-de-paula")]
def get_serializer_class(self) -> type[ModelSerializer]:
if self.action == "create":
return ImportacaoPlanoSaudeDePaulaLinhaCreateSerializer
return ImportacaoPlanoSaudeDePaulaLinhaSerializer
def perform_create(self, serializer: ImportacaoPlanoSaudeDePaulaLinhaCreateSerializer) -> None:
importacao = serializer.validated_data["importacao"]
_garante_importacao_de_paula_em_revisao(importacao)
tipo = serializer.validated_data["tipo_lancamento"]
maior_ordem = ImportacaoPlanoSaudeDePaulaLinha.objects.filter(
importacao=importacao, tipo_lancamento=tipo
).aggregate(Max("ordem"))["ordem__max"]
linha = serializer.save(ordem=0 if maior_ordem is None else maior_ordem + 1)
ImportacaoPlanoSaudeDePaulaAlteracao.objects.create(
importacao=importacao,
tipo=ImportacaoPlanoSaudeDePaulaAlteracao.TIPO_INCLUSAO,
linha=linha,
tipo_lancamento=tipo,
dados_linha=_snapshot_linha_plano_saude(linha),
usuario=self.request.user,
)
def perform_update(self, serializer: ImportacaoPlanoSaudeDePaulaLinhaSerializer) -> None:
linha_anterior = serializer.instance
_garante_importacao_de_paula_em_revisao(linha_anterior.importacao)
valores_anteriores = {
campo: getattr(linha_anterior, campo)
for campo in PLANO_SAUDE_CAMPOS_ALTERACAO
if campo in serializer.validated_data and serializer.validated_data[campo] != getattr(linha_anterior, campo)
}
linha = serializer.save()
for campo, valor_anterior in valores_anteriores.items():
ImportacaoPlanoSaudeDePaulaAlteracao.objects.create(
importacao=linha.importacao,
tipo=ImportacaoPlanoSaudeDePaulaAlteracao.TIPO_EDICAO,
linha=linha,
tipo_lancamento=linha.tipo_lancamento,
campo=campo,
valor_anterior=valor_anterior,
valor_novo=getattr(linha, campo),
dados_linha=_snapshot_linha_plano_saude(linha),
usuario=self.request.user,
)
def perform_destroy(self, instance: ImportacaoPlanoSaudeDePaulaLinha) -> None:
_garante_importacao_de_paula_em_revisao(instance.importacao)
ImportacaoPlanoSaudeDePaulaAlteracao.objects.create(
importacao=instance.importacao,
tipo=ImportacaoPlanoSaudeDePaulaAlteracao.TIPO_EXCLUSAO,
tipo_lancamento=instance.tipo_lancamento,
dados_linha=_snapshot_linha_plano_saude(instance),
usuario=self.request.user,
)
instance.delete()
class ImportacaoPlanoSaudeDePaulaAlteracaoViewSet(viewsets.GenericViewSet):
"""Ver `ImportacaoPlanoSaudeAlteracaoViewSet` — mesma reversão, escopo De Paula."""
http_method_names = ["post", "head", "options"]
queryset = ImportacaoPlanoSaudeDePaulaAlteracao.objects.all()
serializer_class = ImportacaoPlanoSaudeDePaulaAlteracaoSerializer
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("utilitarios", "importacao-plano-saude-de-paula")]
@action(detail=True, methods=["post"])
def reverter(self, request: Request, pk: str | None = None) -> Response:
alteracao = self.get_object()
_garante_importacao_de_paula_em_revisao(alteracao.importacao)
if alteracao.revertida:
raise ValidationError({"detail": "Esta alteração já foi revertida."})
if alteracao.tipo == ImportacaoPlanoSaudeDePaulaAlteracao.TIPO_EDICAO:
if not alteracao.linha_id:
raise ValidationError({"detail": "A linha desta alteração não existe mais."})
setattr(alteracao.linha, alteracao.campo, alteracao.valor_anterior)
alteracao.linha.save(update_fields=[alteracao.campo])
elif alteracao.tipo == ImportacaoPlanoSaudeDePaulaAlteracao.TIPO_INCLUSAO:
if not alteracao.linha_id:
raise ValidationError({"detail": "Esta linha já não existe mais."})
alteracao.linha.delete()
alteracao.linha = None
elif alteracao.tipo == ImportacaoPlanoSaudeDePaulaAlteracao.TIPO_EXCLUSAO:
dados = dict(alteracao.dados_linha or {})
alteracao.linha = ImportacaoPlanoSaudeDePaulaLinha.objects.create(
importacao=alteracao.importacao,
tipo_lancamento=alteracao.tipo_lancamento,
**dados,
)
elif alteracao.tipo == ImportacaoPlanoSaudeDePaulaAlteracao.TIPO_VINCULO_AUTOMATICO:
if not alteracao.linha_id:
raise ValidationError({"detail": "A linha desta alteração não existe mais."})
alteracao.linha.valor_empresa = "0"
alteracao.linha.valor = "0"
alteracao.linha.save(update_fields=["valor_empresa", "valor"])
if alteracao.tipo_lancamento == "mensalidade" and alteracao.importacao.regra_empresa:
_recalcula_familia_regra_empresa_de_paula(alteracao.importacao, alteracao.linha)
if alteracao.vinculo_nome_id:
alteracao.vinculo_nome.delete()
alteracao.vinculo_nome = None
alteracao.revertida = True
alteracao.revertida_em = timezone.now()
alteracao.save()
return Response(ImportacaoPlanoSaudeDePaulaAlteracaoSerializer(alteracao).data)
def _recalcula_familia_regra_empresa_de_paula(
importacao: ImportacaoPlanoSaudeDePaula, linha: ImportacaoPlanoSaudeDePaulaLinha
) -> None:
"""Ver `_recalcula_familia_regra_empresa` — mesma redistribuição por
família, escopo De Paula (`bulk_update` precisa do model certo)."""
regra = planos_saude_regras_empresa.REGRAS_EMPRESA.get(importacao.regra_empresa)
if not regra:
return
linhas_familia = list(
importacao.linhas.filter(
tipo_lancamento=linha.tipo_lancamento, nome_func=linha.nome_func
).order_by("ordem", "id")
)
linhas_e_valores = [(l, parse_valor_br(l.valor_empresa) + parse_valor_br(l.valor)) for l in linhas_familia]
regra["aplica"](linhas_e_valores, linha.tipo_lancamento)
ImportacaoPlanoSaudeDePaulaLinha.objects.bulk_update(linhas_familia, ["valor_empresa", "valor"])
class ImportacaoPlanoSaudeDePaulaAuditoriaViewSet(viewsets.GenericViewSet):
"""Ver `ImportacaoPlanoSaudeAuditoriaViewSet` — mesma resolução manual,
escopo De Paula (grava em `VinculoNomeOperadoraDePaula`, nunca no DE/PARA
da aplicação original)."""
http_method_names = ["post", "head", "options"]
queryset = ImportacaoPlanoSaudeDePaulaAuditoria.objects.all()
serializer_class = ImportacaoPlanoSaudeDePaulaAuditoriaSerializer
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("utilitarios", "importacao-plano-saude-de-paula")]
@action(detail=True, methods=["post"])
def resolver(self, request: Request, pk: str | None = None) -> Response:
item = self.get_object()
_garante_importacao_de_paula_em_revisao(item.importacao)
if item.motivo not in ImportacaoPlanoSaudeDePaulaAuditoria.MOTIVOS_RESOLVIVEIS:
raise ValidationError({"detail": "Esse item de auditoria não pode ser resolvido manualmente."})
if item.resolvida:
raise ValidationError({"detail": "Este item já foi resolvido."})
linha_id = request.data.get("linha_id")
if not linha_id:
raise ValidationError({"linha_id": "Informe a linha da planilha padrão correspondente."})
linha = get_object_or_404(
ImportacaoPlanoSaudeDePaulaLinha, pk=linha_id, importacao_id=item.importacao_id
)
if linha.tipo_lancamento != item.tipo_lancamento:
raise ValidationError({"linha_id": "A linha escolhida não é do mesmo tipo de lançamento deste item."})
linha_eh_titular = not linha.nome_dependente.strip() and not linha.cpf_dependente.strip()
if linha_eh_titular != (item.tipo == "T"):
raise ValidationError(
{"linha_id": "A linha escolhida não corresponde ao mesmo tipo de beneficiário (titular/dependente) deste item."}
)
if linha.valor != "0" or linha.valor_empresa != "0":
raise ValidationError({"linha_id": "Essa linha já tem um valor lançado — escolha uma linha ainda em branco."})
regra_empresa_dados = (
planos_saude_regras_empresa.REGRAS_EMPRESA.get(item.importacao.regra_empresa)
if item.importacao.regra_empresa
else None
)
regra_empresa_cobre_este_tipo = bool(
regra_empresa_dados and item.tipo_lancamento in regra_empresa_dados.get("tipos_lancamento", ("mensalidade",))
)
if regra_empresa_cobre_este_tipo:
linha.valor_empresa = "0"
linha.valor = formata_valor_br(float(item.valor))
linha.save(update_fields=["valor_empresa", "valor"])
_recalcula_familia_regra_empresa_de_paula(item.importacao, linha)
else:
regra_por_pessoa = (item.importacao.custeio_por_tipo or {}).get(item.tipo_lancamento, {})
linha.valor_empresa, linha.valor = planos_saude_matcher.valores_formatados_para_pessoa(
float(item.valor), regra_por_pessoa, item.tipo
)
linha.save(update_fields=["valor_empresa", "valor"])
item.resolvida = True
item.linha_vinculada = linha
item.save(update_fields=["resolvida", "linha_vinculada"])
codigo_empresa_norm = normalizar_codigo_empresa(linha.codigo_empresa)
if codigo_empresa_norm:
destino = {"nome_func_destino": "", "nome_dependente_destino": ""}
if item.tipo == "T":
destino["nome_func_destino"] = linha.nome_func
else:
destino["nome_dependente_destino"] = linha.nome_dependente
VinculoNomeOperadoraDePaula.objects.update_or_create(
operadora=item.importacao.operadora,
codigo_empresa=codigo_empresa_norm,
nome_arquivo_operadora=normaliza_nome(item.nome),
defaults={**destino, "criado_por": request.user},
)
return Response(ImportacaoPlanoSaudeDePaulaAuditoriaSerializer(item).data)
class RegraCusteioPlanoSaudeDePaulaViewSet(viewsets.ModelViewSet):
"""Ver `RegraCusteioPlanoSaudeViewSet` — mesmo cadastro, escopo De Paula."""
http_method_names = ["get", "post", "patch", "delete", "head", "options"]
queryset = RegraCusteioPlanoSaudeDePaula.objects.all()
serializer_class = RegraCusteioPlanoSaudeDePaulaSerializer
def get_permissions(self) -> list[BasePermission]:
return [PermissaoApp("utilitarios", "importacao-plano-saude-de-paula")]
def perform_create(self, serializer: RegraCusteioPlanoSaudeDePaulaSerializer) -> None:
serializer.save(criado_por=self.request.user)
@action(detail=False, methods=["get"], url_path="nome-empresa")
def nome_empresa(self, request: Request) -> Response:
codigo = normalizar_codigo_empresa(request.query_params.get("codigo_empresa") or "")
if not codigo:
return Response({"detail": "Informe codigo_empresa."}, status=status.HTTP_400_BAD_REQUEST)
return Response({"codigo_empresa": codigo, "nome_empresa": resolve_nome_empresa(codigo)})
class IndicadorPercentualTipoViewSet(viewsets.ModelViewSet): class IndicadorPercentualTipoViewSet(viewsets.ModelViewSet):
"""Cadastro dos percentuais individual/grupo/departamento por tipo de """Cadastro dos percentuais individual/grupo/departamento por tipo de
colaborador (ver IndicadorPercentualTipo em models.py) — nunca editado colaborador (ver IndicadorPercentualTipo em models.py) — nunca editado

View File

@ -1,3 +1,21 @@
// Injetado via <script> inline no template, antes deste arquivo, só na
// página "Importação de Plano de Saúde - De Paula" (ver
// templates/importacao-plano-saude-de-paula.html) — na página original
// (importacao-plano-saude.html) window.PID_IPS_CONFIG não existe, então os
// defaults abaixo mantêm o comportamento de sempre.
const PID_IPS_CONFIG = Object.assign(
{
appKey: "importacao-plano-saude",
endpointImportacoes: "/importacoes-plano-saude",
endpointLinhas: "/importacoes-plano-saude-linhas",
endpointAuditoria: "/importacoes-plano-saude-auditoria",
endpointAlteracoes: "/importacoes-plano-saude-alteracoes",
endpointRegrasCusteio: "/regras-custeio-plano-saude",
tituloAjuda: "Importação de Plano de Saúde",
},
window.PID_IPS_CONFIG || {}
);
const PID_IPS_MOTIVO_LABELS = { const PID_IPS_MOTIVO_LABELS = {
VALOR_NEGATIVO: "Valor negativo no mês", VALOR_NEGATIVO: "Valor negativo no mês",
NAO_CADASTRADO: "Não cadastrado no sistema", NAO_CADASTRADO: "Não cadastrado no sistema",
@ -103,31 +121,31 @@ const PID_IPS_AUDITORIA_ACAO_WIDTH = 160;
const PID_IPS_COL_MIN_WIDTH = 60; const PID_IPS_COL_MIN_WIDTH = 60;
async function pidFetchImportacoesPlanoSaude() { async function pidFetchImportacoesPlanoSaude() {
return (await pidApiRequest("/importacoes-plano-saude/")) || []; return (await pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/`)) || [];
} }
async function pidFetchImportacaoPlanoSaude(id) { async function pidFetchImportacaoPlanoSaude(id) {
return pidApiRequest(`/importacoes-plano-saude/${id}/`); return pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/${id}/`);
} }
async function pidFetchOperadorasPlanoSaude() { async function pidFetchOperadorasPlanoSaude() {
return (await pidApiRequest("/importacoes-plano-saude/operadoras/")) || []; return (await pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/operadoras/`)) || [];
} }
async function pidFetchRegrasEmpresaPlanoSaude() { async function pidFetchRegrasEmpresaPlanoSaude() {
return (await pidApiRequest("/importacoes-plano-saude/regras-empresa/")) || []; return (await pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/regras-empresa/`)) || [];
} }
async function pidExcluirImportacaoPlanoSaude(id) { async function pidExcluirImportacaoPlanoSaude(id) {
return pidApiRequest(`/importacoes-plano-saude/${id}/`, { method: "DELETE" }); return pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/${id}/`, { method: "DELETE" });
} }
async function pidCriarImportacaoPlanoSaude(formData) { async function pidCriarImportacaoPlanoSaude(formData) {
return pidApiRequest("/importacoes-plano-saude/", { method: "POST", body: formData }); return pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/`, { method: "POST", body: formData });
} }
async function pidReabrirImportacaoPlanoSaude(id) { async function pidReabrirImportacaoPlanoSaude(id) {
return pidApiRequest(`/importacoes-plano-saude/${id}/reabrir/`, { method: "POST" }); return pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/${id}/reabrir/`, { method: "POST" });
} }
async function pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey) { async function pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey) {
@ -135,53 +153,55 @@ async function pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey) {
formData.append("tipo", tipo); formData.append("tipo", tipo);
formData.append("arquivo", arquivo); formData.append("arquivo", arquivo);
if (operadoraKey) formData.append("operadora", operadoraKey); if (operadoraKey) formData.append("operadora", operadoraKey);
return pidApiRequest("/importacoes-plano-saude/validar-arquivo/", { method: "POST", body: formData }); return pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/validar-arquivo/`, { method: "POST", body: formData });
} }
async function pidAtualizarLinhaPlanoSaude(id, campos) { async function pidAtualizarLinhaPlanoSaude(id, campos) {
return pidApiRequest(`/importacoes-plano-saude-linhas/${id}/`, { method: "PATCH", body: campos }); return pidApiRequest(`${PID_IPS_CONFIG.endpointLinhas}/${id}/`, { method: "PATCH", body: campos });
} }
async function pidCriarLinhaPlanoSaude(importacaoId, tipoLancamento) { async function pidCriarLinhaPlanoSaude(importacaoId, tipoLancamento) {
return pidApiRequest("/importacoes-plano-saude-linhas/", { return pidApiRequest(`${PID_IPS_CONFIG.endpointLinhas}/`, {
method: "POST", method: "POST",
body: { importacao: importacaoId, tipo_lancamento: tipoLancamento }, body: { importacao: importacaoId, tipo_lancamento: tipoLancamento },
}); });
} }
async function pidExcluirLinhaPlanoSaude(id) { async function pidExcluirLinhaPlanoSaude(id) {
return pidApiRequest(`/importacoes-plano-saude-linhas/${id}/`, { method: "DELETE" }); return pidApiRequest(`${PID_IPS_CONFIG.endpointLinhas}/${id}/`, { method: "DELETE" });
} }
async function pidResolverAuditoriaPlanoSaude(itemId, linhaId) { async function pidResolverAuditoriaPlanoSaude(itemId, linhaId) {
return pidApiRequest(`/importacoes-plano-saude-auditoria/${itemId}/resolver/`, { return pidApiRequest(`${PID_IPS_CONFIG.endpointAuditoria}/${itemId}/resolver/`, {
method: "POST", method: "POST",
body: { linha_id: linhaId }, body: { linha_id: linhaId },
}); });
} }
async function pidReverterAlteracaoPlanoSaude(id) { async function pidReverterAlteracaoPlanoSaude(id) {
return pidApiRequest(`/importacoes-plano-saude-alteracoes/${id}/reverter/`, { method: "POST" }); return pidApiRequest(`${PID_IPS_CONFIG.endpointAlteracoes}/${id}/reverter/`, { method: "POST" });
} }
async function pidFetchRegrasCusteioPlanoSaude() { async function pidFetchRegrasCusteioPlanoSaude() {
return (await pidApiRequest("/regras-custeio-plano-saude/")) || []; return (await pidApiRequest(`${PID_IPS_CONFIG.endpointRegrasCusteio}/`)) || [];
} }
async function pidCriarRegraCusteioPlanoSaude(dados) { async function pidCriarRegraCusteioPlanoSaude(dados) {
return pidApiRequest("/regras-custeio-plano-saude/", { method: "POST", body: dados }); return pidApiRequest(`${PID_IPS_CONFIG.endpointRegrasCusteio}/`, { method: "POST", body: dados });
} }
async function pidAtualizarRegraCusteioPlanoSaude(id, dados) { async function pidAtualizarRegraCusteioPlanoSaude(id, dados) {
return pidApiRequest(`/regras-custeio-plano-saude/${id}/`, { method: "PATCH", body: dados }); return pidApiRequest(`${PID_IPS_CONFIG.endpointRegrasCusteio}/${id}/`, { method: "PATCH", body: dados });
} }
async function pidExcluirRegraCusteioPlanoSaude(id) { async function pidExcluirRegraCusteioPlanoSaude(id) {
return pidApiRequest(`/regras-custeio-plano-saude/${id}/`, { method: "DELETE" }); return pidApiRequest(`${PID_IPS_CONFIG.endpointRegrasCusteio}/${id}/`, { method: "DELETE" });
} }
async function pidBuscarNomeEmpresaPlanoSaude(codigoEmpresa) { async function pidBuscarNomeEmpresaPlanoSaude(codigoEmpresa) {
return pidApiRequest(`/regras-custeio-plano-saude/nome-empresa/?codigo_empresa=${encodeURIComponent(codigoEmpresa)}`); return pidApiRequest(
`${PID_IPS_CONFIG.endpointRegrasCusteio}/nome-empresa/?codigo_empresa=${encodeURIComponent(codigoEmpresa)}`
);
} }
/** /**
@ -191,7 +211,7 @@ async function pidBuscarNomeEmpresaPlanoSaude(codigoEmpresa) {
*/ */
async function pidGerarArquivoPlanoSaude(id) { async function pidGerarArquivoPlanoSaude(id) {
await pidEnsureCsrfCookie(); await pidEnsureCsrfCookie();
const response = await fetch(`/api/importacoes-plano-saude/${id}/gerar/`, { const response = await fetch(`/api${PID_IPS_CONFIG.endpointImportacoes}/${id}/gerar/`, {
method: "POST", method: "POST",
credentials: "include", credentials: "include",
headers: { "X-CSRFToken": pidGetCookie("csrftoken") || "" }, headers: { "X-CSRFToken": pidGetCookie("csrftoken") || "" },
@ -230,7 +250,7 @@ document.addEventListener("DOMContentLoaded", async () => {
if (!me) return; if (!me) return;
const utilPerms = (me.permissoes_efetivas && me.permissoes_efetivas["utilitarios"]) || {}; const utilPerms = (me.permissoes_efetivas && me.permissoes_efetivas["utilitarios"]) || {};
const canUse = !!(utilPerms.apps && utilPerms.apps["importacao-plano-saude"]); const canUse = !!(utilPerms.apps && utilPerms.apps[PID_IPS_CONFIG.appKey]);
const noAccessEl = document.getElementById("ips-no-access"); const noAccessEl = document.getElementById("ips-no-access");
const contentEl = document.getElementById("ips-content"); const contentEl = document.getElementById("ips-content");
@ -238,7 +258,7 @@ document.addEventListener("DOMContentLoaded", async () => {
if (contentEl) contentEl.hidden = !canUse; if (contentEl) contentEl.hidden = !canUse;
if (!canUse) return; if (!canUse) return;
pidCriarBotaoAjuda("ips-ajuda-btn", "importacao-plano-saude", "Importação de Plano de Saúde"); pidCriarBotaoAjuda("ips-ajuda-btn", PID_IPS_CONFIG.appKey, PID_IPS_CONFIG.tituloAjuda);
const formView = document.getElementById("ips-form-view"); const formView = document.getElementById("ips-form-view");
const reviewView = document.getElementById("ips-review-view"); const reviewView = document.getElementById("ips-review-view");

View File

@ -151,6 +151,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -152,6 +152,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -151,6 +151,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -0,0 +1,890 @@
{% load static %}
<!DOCTYPE html>
<html lang="pt-BR" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>P.I.D. — Importação de Plano de Saúde - De Paula</title>
<link rel="icon" type="image/svg+xml" href="{% static 'img/pid-icone-escuro.svg' %}" />
<link rel="stylesheet" href="{% static 'css/tokens.css' %}" />
<link rel="stylesheet" href="{% static 'css/base.css' %}" />
<link rel="stylesheet" href="{% static 'css/layout.css' %}" />
<link rel="stylesheet" href="{% static 'css/components.css' %}" />
<link rel="stylesheet" href="{% static 'css/perfis-acesso.css' %}" />
<link rel="stylesheet" href="{% static 'css/importacao-plano-saude.css' %}" />
<script src="{% static 'js/theme.js' %}" defer></script>
</head>
<body>
<div class="app-shell" id="app-shell">
<aside class="sidebar">
<a class="sidebar__brand" href="portal.html" id="sidebar-brand-link" aria-label="Ir para a tela Principal">
<svg class="sidebar__logo sidebar__logo--full" viewBox="0 0 300 80" aria-hidden="true" focusable="false">
<g transform="translate(8,8) scale(1)">
<path d="M10 4 h34 l14 14 v40 a2 2 0 0 1 -2 2 H8 a2 2 0 0 1 -2 -2 V6 a2 2 0 0 1 2 -2 z" fill="#7B5BA8"></path>
<rect x="22" y="4" width="20" height="16" fill="#C6A24A"></rect>
<rect x="34" y="7" width="5" height="10" fill="#7B5BA8"></rect>
<rect x="12" y="26" width="40" height="30" rx="2" fill="#241C33"></rect>
<g class="pid-icon-eye">
<circle cx="24" cy="38" r="5" fill="#F2EDE3"></circle>
<circle cx="26" cy="36.2" r="1.6" fill="#241C33"></circle>
</g>
<g class="pid-icon-eye">
<circle cx="40" cy="38" r="5" fill="#F2EDE3"></circle>
<circle cx="42" cy="36.2" r="1.6" fill="#241C33"></circle>
</g>
<path d="M25 47 q7 6 14 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
</g>
<text x="86" y="42" font-family="'Space Grotesk',Helvetica,Arial,sans-serif" font-size="34" font-weight="700" letter-spacing="1.4" fill="#F2EDE3">P.I.D.</text>
<text x="88" y="61" font-family="'DM Mono',ui-monospace,monospace" font-size="9.5" letter-spacing="2.6" fill="#C6A24A">PORTAL INTERNO DA DE PAULA</text>
</svg>
<svg class="sidebar__logo sidebar__logo--icon" viewBox="0 0 64 64" aria-hidden="true" focusable="false">
<path d="M10 4 h34 l14 14 v40 a2 2 0 0 1 -2 2 H8 a2 2 0 0 1 -2 -2 V6 a2 2 0 0 1 2 -2 z" fill="#7B5BA8"></path>
<rect x="22" y="4" width="20" height="16" fill="#C6A24A"></rect>
<rect x="34" y="7" width="5" height="10" fill="#7B5BA8"></rect>
<rect x="12" y="26" width="40" height="30" rx="2" fill="#241C33"></rect>
<g class="pid-icon-eye">
<circle cx="24" cy="38" r="5" fill="#F2EDE3"></circle>
<circle cx="26" cy="36.2" r="1.6" fill="#241C33"></circle>
</g>
<g class="pid-icon-eye">
<circle cx="40" cy="38" r="5" fill="#F2EDE3"></circle>
<circle cx="42" cy="36.2" r="1.6" fill="#241C33"></circle>
</g>
<path d="M25 47 q7 6 14 0" stroke="#C6A24A" stroke-width="3" fill="none" stroke-linecap="round"></path>
</svg>
</a>
<nav class="sidebar__nav">
<ul class="nav-list">
<li data-section="principal">
<a href="portal.html" class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 11l9-7 9 7v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Principal</span>
</a>
</li>
<li data-section="ramais">
<a href="ramais.html" class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4c0 9 7 16 16 16l3-4-6-3-2 2c-3-1.5-5-3.5-6-6l2-2-3-6z" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Ramais</span>
</a>
</li>
<li data-section="calendario">
<a href="#" class="nav-item" id="calendario-depaula-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 9h18M8 3v4M16 3v4" stroke-linecap="round"/></svg>
<span class="nav-item__label">Calendário De Paula</span>
</a>
</li>
<li data-section="calendario-individual">
<a href="calendario-individual.html" class="nav-item">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 9h18M8 3v4M16 3v4" stroke-linecap="round"/><path d="M9 15l2 2 4-4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Calendário Individual</span>
</a>
</li>
<li data-section="portais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
<span class="nav-item__label">Portais</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="portal-do-cliente"><a href="#" class="nav-subitem">Portal do Cliente</a></li>
<li data-app="portal-fiscal"><a href="#" class="nav-subitem">Portal Fiscal</a></li>
</ul>
</li>
<li data-section="geradoc" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M14 3v4h4" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Geradoc</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="simulacao-custo-contratacao"><a href="custo-contratacao.html" class="nav-subitem">Simulação de Custo de Contratação</a></li>
<li data-app="indicador-desempenho"><a href="indicador-desempenho.html" class="nav-subitem">Indicador de Desempenho</a></li>
</ul>
</li>
<li data-section="relatorios" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20V10M12 20V4M20 20v-7" stroke-linecap="round"/></svg>
<span class="nav-item__label">Relatórios</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="relatorio-setorial"><a href="#" class="nav-subitem">Relatório Setorial</a></li>
<li data-app="indicadores"><a href="#" class="nav-subitem">Indicadores</a></li>
<li class="nav-subgroup" data-subgroup="qualidade">
<button type="button" class="nav-subitem nav-subgroup__toggle" data-subgroup-toggle>
<span>Qualidade</span>
<svg class="nav-item__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-subgroup__list">
<li data-app="nao-conformidades">
<a href="nao-conformidades.html" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Não Conformidades
</a>
</li>
</ul>
</li>
</ul>
</li>
<li data-section="relatorios-gerenciais" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18M7 15l4-5 3 3 5-7" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Relatórios Gerenciais</span>
<span class="badge-restricted" data-restricted-badge hidden>Restrito</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="visao-diretoria"><a href="#" class="nav-subitem">Visão Diretoria</a></li>
<li data-app="visao-gerencial"><a href="#" class="nav-subitem">Visão Gerencial</a></li>
</ul>
</li>
<li data-section="utilitarios" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 7l3 3-8 8-4 1 1-4z" stroke-linejoin="round"/><path d="M12 9l3 3" stroke-linecap="round"/></svg>
<span class="nav-item__label">Utilitários</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem is-active">Importação de Plano de Saúde - De Paula</a></li>
</ul>
</li>
<li data-section="integracoes" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="12" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="18" cy="18" r="3"/><path d="M8.6 10.6L15.4 7.4M8.6 13.4l6.8 3.2" stroke-linecap="round"/></svg>
<span class="nav-item__label">Integrações</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="questor"><a href="#" class="nav-subitem">Questor</a></li>
</ul>
</li>
<li data-section="auditorias" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l7 4v5c0 5-3.5 8-7 9-3.5-1-7-4-7-9V7z" stroke-linejoin="round"/><path d="M9 12l2 2 4-4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Auditorias</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li class="nav-subgroup" data-subgroup="consultoria-tributaria">
<button type="button" class="nav-subitem nav-subgroup__toggle" data-subgroup-toggle>
<span>Consultoria Tributária</span>
<svg class="nav-item__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-subgroup__list">
<li data-app="controle-simples-nacional">
<a href="#" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Controle Simples Nacional
</a>
</li>
</ul>
</li>
<li class="nav-subgroup" data-subgroup="fisco-contabil">
<button type="button" class="nav-subitem nav-subgroup__toggle" data-subgroup-toggle>
<span>Fisco/Contábil</span>
<svg class="nav-item__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-subgroup__list">
<li data-app="auditoria-de-balancetes">
<a href="#" class="nav-subitem nav-subitem--tool">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/><path d="M14 2v4h4" stroke-linejoin="round"/></svg>
Auditoria de Balancetes
</a>
</li>
</ul>
</li>
</ul>
</li>
<li data-section="links-ferramentas" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg>
<span class="nav-item__label">Links &amp; Ferramentas</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="links-ferramentas-visualizar"><a href="links-ferramentas.html" class="nav-subitem">Links &amp; Ferramentas</a></li>
<li data-app="acessos-gerais-visualizar"><a href="acessos-gerais.html" class="nav-subitem">Acessos Gerais</a></li>
</ul>
</li>
<li data-section="solicitacoes" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 10h.01M12 10h.01M16 10h.01M21 12a9 9 0 1 1-4.4-7.7L21 3l-1.2 4.5A8.96 8.96 0 0 1 21 12z"/></svg>
<span class="nav-item__label">Solicitações</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-app="controle-notificacoes"><a href="https://form.asana.com/?k=PcELrDLzN969CwpiRBLtFA&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">Controle de Notificações</a></li>
<li data-app="ordem-servico"><a href="#" class="nav-subitem">Ordem de Serviço</a></li>
<li data-app="pimi"><a href="https://form.asana.com/?k=XfBww25lGUyPVrInNcLyuQ&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">P.I.M.I.</a></li>
<li data-app="programa-indicacoes"><a href="https://form.asana.com/?k=y0mTeaRTOAoC0r0XP8ee0Q&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">Programa de Indicações</a></li>
<li data-app="pos-vendas"><a href="https://form.asana.com/?k=jRAztLip-_BknWPsGmEcZA&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">Pós-Vendas</a></li>
<li data-app="sugestao-inovacao"><a href="https://form.asana.com/?k=Zn8cLeT97R7FTUrsNB8Yaw&amp;d=1207308016523218" target="_blank" rel="noopener noreferrer" class="nav-subitem">Sugestão de Inovação</a></li>
</ul>
</li>
<li data-section="administracao" class="nav-group">
<button type="button" class="nav-item nav-group__toggle" data-group-toggle>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z" stroke-linejoin="round"/></svg>
<span class="nav-item__label">Administração</span>
<svg class="nav-item__chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<ul class="nav-group__list">
<li data-section="perfis-acesso"><a href="perfis-acesso.html" class="nav-subitem">Perfis de Acesso</a></li>
<li data-section="usuarios"><a href="usuarios.html" class="nav-subitem">Usuários</a></li>
</ul>
</li>
</ul>
</nav>
</aside>
<div class="main-content">
<header class="topbar">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Alternar menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16" stroke-linecap="round"/></svg>
</button>
<h1 class="topbar__title">Importação de Plano de Saúde - De Paula</h1>
<div class="topbar__actions">
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Ativar modo claro">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" stroke-linecap="round"/></svg>
</button>
<div class="notifications">
<button type="button" class="icon-btn" id="notif-bell" aria-label="Notificações">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 8a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6" stroke-linejoin="round"/><path d="M9.5 20a2.5 2.5 0 0 0 5 0"/></svg>
<span class="notif-badge" id="notif-badge" hidden>0</span>
</button>
<div class="notif-dropdown" id="notif-dropdown" hidden>
<div class="notif-dropdown__header">
<span>Notificações</span>
<div class="notif-dropdown__header-actions">
<button type="button" id="notif-history-btn">Histórico</button>
<button type="button" id="notif-clear-all">Limpar tudo</button>
</div>
</div>
<ul class="notif-list" id="notif-list"></ul>
</div>
</div>
<div class="account-menu">
<button type="button" class="persona-chip" id="persona-chip">
<span class="persona-chip__icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="7" r="4"/></svg>
</span>
<span id="persona-chip-label">Usuário</span>
</button>
<div class="account-dropdown" id="account-dropdown" hidden>
<div class="account-dropdown__header">
<p class="account-dropdown__name" id="account-name"></p>
<p class="account-dropdown__profile" id="account-profile"></p>
</div>
<button type="button" class="account-dropdown__item" id="account-manage-account-btn">Gerenciar Usuário</button>
<a href="perfis-acesso.html" class="account-dropdown__item" id="account-manage-link" hidden>Gerenciar Perfis de Acesso</a>
<div class="account-dropdown__section">
<p class="account-dropdown__section-title">Tema de cores</p>
<div class="theme-swatches" id="theme-swatches"></div>
</div>
<button type="button" class="account-dropdown__item account-dropdown__logout" data-logout>Sair</button>
</div>
</div>
</div>
</header>
<main class="page-content page-content--wide">
<div id="ips-no-access" class="no-access" hidden>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="4" y="10" width="16" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3" stroke-linecap="round" stroke-linejoin="round"/></svg>
<h2>Sem acesso a Importação de Plano de Saúde - De Paula</h2>
<p>Seu perfil de acesso não tem permissão para visualizar esta tela. Solicite à Inovação a liberação em Perfis de Acesso.</p>
</div>
<div id="ips-content">
<div id="ips-list-view">
<div class="ips-header">
<div>
<div class="ips-title-row">
<h2>Importação de Plano de Saúde - De Paula</h2>
<span class="info-tooltip">
<button type="button" class="info-tooltip__trigger" id="ips-ajuda-btn" aria-label="Mais informações sobre esta ferramenta">?</button>
<span class="info-tooltip__text" role="tooltip">Mais informações</span>
</span>
</div>
<p class="ips-subtitle">Gere o arquivo de lançamento de mensalidade/coparticipação no leiaute do sistema, a partir do relatório da operadora — plano de saúde dos colaboradores do escritório, separado das importações de clientes.</p>
</div>
<div class="ips-header-actions">
<button type="button" class="btn-outline" id="ips-regracad-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3h9l3 3v15a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M14 3v4h4" stroke-linejoin="round"/></svg>
Cadastro de Regras
</button>
<button type="button" class="btn-solid" id="ips-new-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14" stroke-linecap="round"/></svg>
Nova Importação
</button>
</div>
</div>
<div class="ips-list-bulk-actions" id="ips-list-bulk-actions" hidden>
<span id="ips-list-bulk-count"></span>
<button type="button" class="btn-danger-outline" id="ips-list-bulk-delete-btn">Excluir selecionadas</button>
</div>
<div class="pa-table-wrap ips-list-table-wrap">
<table class="pa-table" id="ips-list-table">
<thead>
<tr>
<th class="ips-select-col"><input type="checkbox" id="ips-list-select-all" aria-label="Selecionar todos os registros desta página" /></th>
<th data-sort="codigo_empresa">Cód. Empresa <span class="ips-sort-icon">↕</span></th>
<th data-sort="nome_operadora">Operadora <span class="ips-sort-icon">↕</span></th>
<th data-sort="tipos">Tipos <span class="ips-sort-icon">↕</span></th>
<th data-sort="status">Status <span class="ips-sort-icon">↕</span></th>
<th data-sort="criado_por">Criado por <span class="ips-sort-icon">↕</span></th>
<th data-sort="criado_em">Data <span class="ips-sort-icon">↕</span></th>
<th>
<button type="button" class="ips-list-reset-btn" id="ips-list-reset-btn" title="Limpar filtros e ordenação" aria-label="Limpar filtros e ordenação">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 20H8l-5.5-5.5a2 2 0 0 1 0-2.8l9-9a2 2 0 0 1 2.8 0l6.2 6.2a2 2 0 0 1 0 2.8L13 19" stroke-linejoin="round"/><path d="M8.5 12.5l6 6" stroke-linecap="round"/></svg>
</button>
</th>
</tr>
</thead>
<tbody id="ips-list-body"></tbody>
</table>
</div>
<p class="ips-empty" id="ips-list-empty" hidden>Nenhuma importação realizada ainda.</p>
<div class="ips-list-pagination" id="ips-list-pagination"></div>
<p class="ips-list-count" id="ips-list-count"></p>
</div>
<div id="ips-form-view" hidden>
<div class="ips-header">
<div>
<h2>Nova Importação</h2>
<p class="ips-subtitle">Busque a planilha padrão do Questor por competência (ou anexe manualmente) e informe o relatório enviado pela operadora.</p>
</div>
<button type="button" class="btn-outline" id="ips-form-back-btn">Voltar</button>
</div>
<div class="ips-form">
<div class="modal-field">
<label for="ips-imp-empresa-search">Empresa</label>
<p class="ips-regra-actions__hint">Só aparecem empresas com regra de custeio já cadastrada — cadastre ou edite uma regra em "Cadastro de Regras" antes de importar uma empresa nova.</p>
<div class="ips-combo" id="ips-imp-empresa-combo">
<input type="text" id="ips-imp-empresa-search" class="ips-combo__input" placeholder="Selecionar empresa..." autocomplete="off" />
<input type="hidden" id="ips-imp-empresa-valor" />
<button type="button" class="ips-combo__clear" id="ips-imp-empresa-limpar-btn" aria-label="Limpar seleção de empresa" title="Limpar seleção" hidden>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
<button type="button" class="ips-combo__toggle" id="ips-imp-empresa-toggle-btn" aria-label="Ver todas as empresas cadastradas" title="Ver todas as empresas cadastradas">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ips-combo__list" id="ips-imp-empresa-combo-list" hidden></div>
</div>
</div>
<div class="modal-field">
<label for="ips-imp-operadora-search">Operadora</label>
<div class="ips-combo" id="ips-imp-operadora-combo">
<input type="text" id="ips-imp-operadora-search" class="ips-combo__input" placeholder="Selecione a empresa primeiro..." autocomplete="off" disabled />
<input type="hidden" id="ips-form-operadora" />
<button type="button" class="ips-combo__clear" id="ips-imp-operadora-limpar-btn" aria-label="Limpar seleção de operadora" title="Limpar seleção" hidden>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
<button type="button" class="ips-combo__toggle" id="ips-imp-operadora-toggle-btn" aria-label="Ver todas as operadoras cadastradas para esta empresa" title="Ver todas as operadoras cadastradas para esta empresa" disabled>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ips-combo__list" id="ips-imp-operadora-combo-list" hidden></div>
</div>
</div>
<div class="ips-imp-resumo" id="ips-imp-resumo" hidden>
<p class="ips-imp-resumo__tipos" id="ips-imp-resumo-tipos"></p>
<p class="ips-imp-resumo__linha" id="ips-imp-resumo-mensalidade" hidden></p>
<p class="ips-imp-resumo__linha" id="ips-imp-resumo-coparticipacao" hidden></p>
<p class="ips-imp-resumo__obs" id="ips-imp-resumo-observacoes" hidden></p>
</div>
<div class="modal-field">
<label>Arquivos</label>
<div class="ips-upload-box">
<p class="ips-upload-box__title">1. Planilha padrão</p>
<p class="ips-upload-box__hint">Cadastro dos beneficiários (sem valores) — buscado direto do Questor, ou anexado manualmente como alternativa.</p>
<div class="ips-planilha-origem-opcoes">
<label class="modal-checkbox">
<input type="radio" name="ips-planilha-origem" id="ips-planilha-origem-questor" value="questor" checked />
Buscar automaticamente do Questor
</label>
<label class="modal-checkbox">
<input type="radio" name="ips-planilha-origem" id="ips-planilha-origem-upload" value="upload" />
Anexar manualmente
</label>
</div>
<div id="ips-planilha-questor-field" class="modal-field">
<label for="ips-form-competencia">Competência</label>
<input type="text" id="ips-form-competencia" inputmode="numeric" placeholder="MM/AAAA" maxlength="7" autocomplete="off" />
</div>
<div class="ips-file-field" id="ips-planilha-upload-field" hidden>
<label class="btn-outline ips-file-field__btn" for="ips-form-planilha">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 16V4M12 4l4 4M12 4L8 8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" stroke-linecap="round" stroke-linejoin="round"/></svg>
Selecionar arquivo
</label>
<input type="file" id="ips-form-planilha" accept=".csv" class="ips-file-field__input" />
<span class="ips-file-field__name" id="ips-form-planilha-name">Nenhum arquivo selecionado</span>
<button type="button" class="icon-btn ips-file-field__remove" id="ips-form-planilha-remove" aria-label="Remover arquivo" title="Remover arquivo" hidden>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
</div>
<p class="ips-file-field__status" id="ips-form-planilha-status" hidden></p>
</div>
<div class="ips-upload-box">
<p class="ips-upload-box__title">2. Arquivos da operadora</p>
<p class="ips-upload-box__hint">Relatório de faturamento enviado pela operadora do plano (PDF, CSV ou XLSX), com os valores do mês. Algumas operadoras mandam mensalidade e coparticipação em arquivos separados, o tipo de cada um é identificado automaticamente.</p>
<div class="ips-file-field">
<label class="btn-outline ips-file-field__btn" for="ips-form-arquivo">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 16V4M12 4l4 4M12 4L8 8" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" stroke-linecap="round" stroke-linejoin="round"/></svg>
Selecionar arquivo(s)
</label>
<input type="file" id="ips-form-arquivo" accept=".csv,.pdf,.xlsx" class="ips-file-field__input" multiple />
</div>
<ul class="ips-arquivo-list" id="ips-form-arquivo-list"></ul>
</div>
</div>
<p class="modal-error" id="ips-form-error"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" id="ips-form-cancel-btn">Cancelar</button>
<button type="button" class="btn-solid" id="ips-form-submit-btn">Processar</button>
</div>
</div>
</div>
<div id="ips-review-view" hidden>
<div class="ips-header">
<div>
<h2>Revisão <span class="ips-review-empresa" id="ips-review-empresa" hidden></span></h2>
<p class="ips-subtitle" id="ips-review-subtitle"></p>
</div>
<div class="ips-header-actions">
<button type="button" class="btn-outline" id="ips-review-back-btn">Voltar</button>
<button type="button" class="btn-outline" id="ips-review-editar-btn" hidden>Editar</button>
<button type="button" class="btn-solid" id="ips-gerar-btn">Gerar Arquivo</button>
</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>
<div class="pa-tabs" id="ips-tabs"></div>
<div id="ips-tab-panels"></div>
</div>
</div>
</main>
</div>
</div>
<div class="modal-overlay" id="ips-vincular-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title">Vincular pessoa</h2>
<p class="ips-vincular-resumo" id="ips-vincular-resumo"></p>
<div class="modal-field">
<label for="ips-vincular-search">Buscar na planilha padrão</label>
<input type="text" id="ips-vincular-search" class="checklist-search" placeholder="Buscar por nome..." autocomplete="off" />
</div>
<div class="checklist-box" id="ips-vincular-lista"></div>
<p class="modal-error" id="ips-vincular-error"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" id="ips-vincular-cancel-btn">Cancelar</button>
<button type="button" class="btn-solid" id="ips-vincular-confirm-btn" disabled>Vincular</button>
</div>
</div>
</div>
<div class="modal-overlay" id="ips-regra-empresa-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title">Selecionar regra empresa</h2>
<p class="ips-regra-actions__hint">Regras especiais cadastradas diretamente pela Inovação — cada uma substitui o custeio manual só do(s) tipo(s) de lançamento que ela cobre (mensalidade e/ou coparticipação, varia por regra); os demais tipos selecionados continuam usando o custeio configurado manualmente abaixo.</p>
<div class="checklist-box ips-regras-lista" id="ips-regra-empresa-lista"></div>
<p class="modal-error" id="ips-regra-empresa-error"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" id="ips-regra-empresa-cancel-btn">Fechar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="ips-regracad-modal" hidden>
<div class="modal-card modal-card--wide">
<h2 class="modal-card__title">Cadastro de Regras de Custeio</h2>
<p class="ips-regra-actions__hint">Cadastre e edite aqui as regras de custeio por empresa e operadora — a tela de Nova Importação só aplica uma regra já cadastrada, sem editar nada.</p>
<div class="ips-regracad-toolbar" id="ips-regracad-toolbar">
<div class="ips-regracad-toolbar__row">
<div class="modal-field">
<label for="ips-regracad-empresa-search">Empresa</label>
<div class="ips-combo" id="ips-regracad-empresa-combo">
<input type="text" id="ips-regracad-empresa-search" class="ips-combo__input" placeholder="Selecionar empresa..." autocomplete="off" />
<input type="hidden" id="ips-regracad-empresa-valor" />
<button type="button" class="ips-combo__clear" id="ips-regracad-empresa-limpar-btn" aria-label="Limpar seleção de empresa" title="Limpar seleção" hidden>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
<button type="button" class="ips-combo__toggle" id="ips-regracad-empresa-toggle-btn" aria-label="Ver todas as empresas cadastradas" title="Ver todas as empresas cadastradas">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ips-combo__list" id="ips-regracad-empresa-combo-list" hidden></div>
</div>
</div>
<button type="button" class="btn-outline" id="ips-regracad-nova-btn">+ Nova regra</button>
</div>
<div class="ips-regracad-toolbar__row">
<div class="modal-field">
<label for="ips-regracad-operadora-search">Operadora</label>
<div class="ips-combo" id="ips-regracad-operadora-combo">
<input type="text" id="ips-regracad-operadora-search" class="ips-combo__input" placeholder="Selecione a empresa primeiro..." autocomplete="off" disabled />
<input type="hidden" id="ips-regracad-operadora-valor" />
<button type="button" class="ips-combo__clear" id="ips-regracad-operadora-limpar-btn" aria-label="Limpar seleção de operadora" title="Limpar seleção" hidden>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
<button type="button" class="ips-combo__toggle" id="ips-regracad-operadora-toggle-btn" aria-label="Ver todas as operadoras cadastradas para esta empresa" title="Ver todas as operadoras cadastradas para esta empresa" disabled>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ips-combo__list" id="ips-regracad-operadora-combo-list" hidden></div>
</div>
</div>
<button type="button" class="btn-outline" id="ips-regracad-nova-operadora-btn" hidden>+ Nova operadora</button>
</div>
</div>
<p class="ips-empty" id="ips-regracad-vazio">Selecione uma empresa e uma operadora pra visualizar/editar a regra, ou clique em "+ Nova regra".</p>
<p class="ips-regracad-modo" id="ips-regracad-modo" hidden></p>
<div class="ips-regracad-novo" id="ips-regracad-novo-view" hidden>
<div class="ips-regracad-novo__empresa-row">
<div class="modal-field">
<label for="ips-regracad-novo-codigo-empresa">Código da empresa</label>
<input type="text" id="ips-regracad-novo-codigo-empresa" placeholder="Ex.: 92" autocomplete="off" />
</div>
<p class="ips-regracad-novo__empresa-nome" id="ips-regracad-novo-empresa-nome" hidden></p>
</div>
<div class="modal-field">
<label for="ips-regracad-novo-operadora-search">Operadora</label>
<div class="ips-combo" id="ips-regracad-novo-operadora-combo">
<input type="text" id="ips-regracad-novo-operadora-search" class="ips-combo__input" placeholder="Selecionar operadora..." autocomplete="off" />
<input type="hidden" id="ips-regracad-novo-operadora-valor" />
<button type="button" class="ips-combo__toggle" id="ips-regracad-novo-operadora-toggle-btn" aria-label="Ver todas as operadoras" title="Ver todas as operadoras">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="ips-combo__list" id="ips-regracad-novo-operadora-combo-list" hidden></div>
</div>
<p class="modal-error" id="ips-regracad-novo-operadora-duplicada" hidden>Já existe uma regra cadastrada para esta empresa com esta operadora — edite a regra existente em vez de criar outra.</p>
</div>
</div>
<div class="ips-regracad-form" id="ips-regracad-form-view" hidden>
<div class="modal-field ips-tipo-field">
<label>Tipo de importação</label>
<div class="ips-tipo-option">
<label class="modal-checkbox">
<input type="checkbox" id="ips-form-tipo-mensalidade" />
Mensalidade
</label>
<div class="ips-custeio-radios" id="ips-custeio-mensalidade" hidden>
<div class="ips-custeio-pessoa">
<p class="ips-custeio-pessoa__label">Titular</p>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-mensalidade-titular" value="empresa" /> Custeado pela empresa</label>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-mensalidade-titular" value="empregado" /> Descontado do empregado</label>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-mensalidade-titular" value="especifica" /> Regra específica</label>
<div class="ips-regra-especifica" id="ips-regra-especifica-mensalidade-titular" hidden>
<div class="modal-field">
<label for="ips-limite-mensalidade-titular">Limite de valor custeado pela empresa</label>
<input type="text" id="ips-limite-mensalidade-titular" placeholder="Ex.: 150,00" autocomplete="off" />
</div>
<div class="modal-field">
<label for="ips-percentual-mensalidade-titular">% de custeio da empresa</label>
<input type="text" id="ips-percentual-mensalidade-titular" placeholder="Ex.: 50" autocomplete="off" />
</div>
<p class="ips-regra-especifica__hint">Preencha um dos dois campos ou os dois — se os dois forem preenchidos, vale o que resultar no menor valor custeado pela empresa. O excedente/restante vira desconto do empregado.</p>
<div class="ips-regra-especifica__alt">
<div class="modal-field">
<label for="ips-limite-desconto-mensalidade-titular">Limite de desconto do empregado</label>
<input type="text" id="ips-limite-desconto-mensalidade-titular" placeholder="Ex.: 10,00" autocomplete="off" />
</div>
<p class="ips-regra-especifica__hint">Alternativa aos campos acima: define o valor máximo descontado do empregado — a empresa custeia o restante. Não pode ser combinado com o limite de valor/percentual (preencher este campo desabilita os outros dois, e vice-versa).</p>
</div>
</div>
</div>
<div class="ips-custeio-pessoa">
<p class="ips-custeio-pessoa__label">Dependente</p>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-mensalidade-dependente" value="empresa" /> Custeado pela empresa</label>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-mensalidade-dependente" value="empregado" /> Descontado do empregado</label>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-mensalidade-dependente" value="especifica" /> Regra específica</label>
<div class="ips-regra-especifica" id="ips-regra-especifica-mensalidade-dependente" hidden>
<div class="modal-field">
<label for="ips-limite-mensalidade-dependente">Limite de valor custeado pela empresa</label>
<input type="text" id="ips-limite-mensalidade-dependente" placeholder="Ex.: 150,00" autocomplete="off" />
</div>
<div class="modal-field">
<label for="ips-percentual-mensalidade-dependente">% de custeio da empresa</label>
<input type="text" id="ips-percentual-mensalidade-dependente" placeholder="Ex.: 50" autocomplete="off" />
</div>
<p class="ips-regra-especifica__hint">Preencha um dos dois campos ou os dois — se os dois forem preenchidos, vale o que resultar no menor valor custeado pela empresa. O excedente/restante vira desconto do empregado.</p>
<div class="ips-regra-especifica__alt">
<div class="modal-field">
<label for="ips-limite-desconto-mensalidade-dependente">Limite de desconto do empregado</label>
<input type="text" id="ips-limite-desconto-mensalidade-dependente" placeholder="Ex.: 10,00" autocomplete="off" />
</div>
<p class="ips-regra-especifica__hint">Alternativa aos campos acima: define o valor máximo descontado do empregado — a empresa custeia o restante. Não pode ser combinado com o limite de valor/percentual (preencher este campo desabilita os outros dois, e vice-versa).</p>
</div>
</div>
</div>
</div>
</div>
<div class="ips-tipo-option">
<label class="modal-checkbox">
<input type="checkbox" id="ips-form-tipo-coparticipacao" />
Coparticipação
</label>
<div class="ips-custeio-radios" id="ips-custeio-coparticipacao" hidden>
<div class="ips-custeio-pessoa">
<p class="ips-custeio-pessoa__label">Titular</p>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-coparticipacao-titular" value="empresa" /> Custeado pela empresa</label>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-coparticipacao-titular" value="empregado" /> Descontado do empregado</label>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-coparticipacao-titular" value="especifica" /> Regra específica</label>
<div class="ips-regra-especifica" id="ips-regra-especifica-coparticipacao-titular" hidden>
<div class="modal-field">
<label for="ips-limite-coparticipacao-titular">Limite de valor custeado pela empresa</label>
<input type="text" id="ips-limite-coparticipacao-titular" placeholder="Ex.: 150,00" autocomplete="off" />
</div>
<div class="modal-field">
<label for="ips-percentual-coparticipacao-titular">% de custeio da empresa</label>
<input type="text" id="ips-percentual-coparticipacao-titular" placeholder="Ex.: 50" autocomplete="off" />
</div>
<p class="ips-regra-especifica__hint">Preencha um dos dois campos ou os dois — se os dois forem preenchidos, vale o que resultar no menor valor custeado pela empresa. O excedente/restante vira desconto do empregado.</p>
<div class="ips-regra-especifica__alt">
<div class="modal-field">
<label for="ips-limite-desconto-coparticipacao-titular">Limite de desconto do empregado</label>
<input type="text" id="ips-limite-desconto-coparticipacao-titular" placeholder="Ex.: 10,00" autocomplete="off" />
</div>
<p class="ips-regra-especifica__hint">Alternativa aos campos acima: define o valor máximo descontado do empregado — a empresa custeia o restante. Não pode ser combinado com o limite de valor/percentual (preencher este campo desabilita os outros dois, e vice-versa).</p>
</div>
</div>
</div>
<div class="ips-custeio-pessoa">
<p class="ips-custeio-pessoa__label">Dependente</p>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-coparticipacao-dependente" value="empresa" /> Custeado pela empresa</label>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-coparticipacao-dependente" value="empregado" /> Descontado do empregado</label>
<label class="modal-checkbox"><input type="radio" name="ips-custeio-coparticipacao-dependente" value="especifica" /> Regra específica</label>
<div class="ips-regra-especifica" id="ips-regra-especifica-coparticipacao-dependente" hidden>
<div class="modal-field">
<label for="ips-limite-coparticipacao-dependente">Limite de valor custeado pela empresa</label>
<input type="text" id="ips-limite-coparticipacao-dependente" placeholder="Ex.: 150,00" autocomplete="off" />
</div>
<div class="modal-field">
<label for="ips-percentual-coparticipacao-dependente">% de custeio da empresa</label>
<input type="text" id="ips-percentual-coparticipacao-dependente" placeholder="Ex.: 50" autocomplete="off" />
</div>
<p class="ips-regra-especifica__hint">Preencha um dos dois campos ou os dois — se os dois forem preenchidos, vale o que resultar no menor valor custeado pela empresa. O excedente/restante vira desconto do empregado.</p>
<div class="ips-regra-especifica__alt">
<div class="modal-field">
<label for="ips-limite-desconto-coparticipacao-dependente">Limite de desconto do empregado</label>
<input type="text" id="ips-limite-desconto-coparticipacao-dependente" placeholder="Ex.: 10,00" autocomplete="off" />
</div>
<p class="ips-regra-especifica__hint">Alternativa aos campos acima: define o valor máximo descontado do empregado — a empresa custeia o restante. Não pode ser combinado com o limite de valor/percentual (preencher este campo desabilita os outros dois, e vice-versa).</p>
</div>
</div>
</div>
</div>
</div>
<div class="ips-tipo-option">
<label class="modal-checkbox">
<input type="checkbox" id="ips-form-tipo-regra-empresa" />
Regra especial da empresa
</label>
<div class="ips-regra-empresa-box" id="ips-regra-empresa-box" hidden>
<p class="ips-regra-empresa-box__hint">Os tipos de lançamento cobertos por esta regra (mensalidade e/ou coparticipação, conforme a regra escolhida) são custeados automaticamente por ela — substitui o custeio manual de titular/dependente acima, só para esses tipos.</p>
<p class="ips-regra-empresa-box__atual" id="ips-regra-empresa-atual">Nenhuma regra selecionada.</p>
<button type="button" class="btn-outline" id="ips-regra-empresa-selecionar-btn">Selecionar regra</button>
</div>
</div>
</div>
<div class="modal-field">
<label for="ips-regracad-observacoes">Observações</label>
<textarea id="ips-regracad-observacoes" rows="3" placeholder="Ex.: Empresa não desconta plano do empregado XX."></textarea>
</div>
</div>
<p class="modal-error" id="ips-regracad-error"></p>
<div class="modal-actions modal-actions--spread">
<div class="modal-actions__group">
<button type="button" class="btn-danger-outline" id="ips-regracad-excluir-btn" hidden>Excluir regra</button>
</div>
<div class="modal-actions__group">
<button type="button" class="btn-outline" id="ips-regracad-fechar-btn">Fechar</button>
<button type="button" class="btn-outline" id="ips-regracad-novo-cancelar-btn" hidden>Cancelar</button>
<button type="button" class="btn-solid" id="ips-regracad-salvar-btn" hidden>Salvar alterações</button>
<button type="button" class="btn-solid" id="ips-regracad-criar-btn" hidden>Criar regra</button>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="manage-account-modal" hidden>
<div class="modal-card" id="manage-account-modal-card">
<h2 class="modal-card__title">Gerenciar Usuário</h2>
<div class="modal-field">
<button type="button" class="btn-outline" id="manage-account-change-password-btn">Alterar senha</button>
</div>
<div class="modal-field" id="manage-account-liderados-field" hidden>
<label>Usuários sob sua liderança</label>
<div class="dual-select">
<div class="dual-select__panel">
<div class="dual-select__panel-header">
<button type="button" class="btn-solid" id="manage-account-liderados-available-link-btn" disabled>Vincular</button>
</div>
<div class="dual-select__table-wrap">
<table class="dual-select__table">
<thead>
<tr>
<th><input type="checkbox" id="manage-account-liderados-available-select-all" aria-label="Selecionar todos" /></th>
<th class="dual-select__sort" id="manage-account-liderados-available-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
<th>Departamento</th>
</tr>
<tr class="dual-select__filters">
<th></th>
<th><input type="text" id="manage-account-liderados-available-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
<th><input type="text" id="manage-account-liderados-available-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
</tr>
</thead>
<tbody id="manage-account-liderados-available-body"></tbody>
</table>
</div>
<div class="dual-select__footer" id="manage-account-liderados-available-footer"></div>
</div>
<div class="dual-select__panel">
<div class="dual-select__panel-header">
<button type="button" class="btn-danger-outline" id="manage-account-liderados-linked-unlink-btn" disabled>Desvincular</button>
</div>
<div class="dual-select__table-wrap">
<table class="dual-select__table">
<thead>
<tr>
<th><input type="checkbox" id="manage-account-liderados-linked-select-all" aria-label="Selecionar todos" /></th>
<th class="dual-select__sort" id="manage-account-liderados-linked-sort-nome">Nome <span class="dual-select__sort-icon">↕</span></th>
<th>Departamento</th>
</tr>
<tr class="dual-select__filters">
<th></th>
<th><input type="text" id="manage-account-liderados-linked-search-nome" placeholder="Buscar por nome..." autocomplete="off" /></th>
<th><input type="text" id="manage-account-liderados-linked-search-departamento" placeholder="Buscar por departamento..." autocomplete="off" /></th>
</tr>
</thead>
<tbody id="manage-account-liderados-linked-body"></tbody>
</table>
</div>
<div class="dual-select__footer" id="manage-account-liderados-linked-footer"></div>
</div>
</div>
<p class="modal-error" id="manage-account-liderados-error"></p>
</div>
<div class="modal-actions">
<button type="button" class="btn-outline" id="manage-account-close-btn">Fechar</button>
<button type="button" class="btn-solid" id="manage-account-save-btn" hidden>Salvar liderados</button>
</div>
</div>
</div>
<div class="modal-overlay" id="password-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title">Alterar senha</h2>
<div class="modal-field">
<label for="pwd-current">Senha atual</label>
<div class="password-field">
<input type="password" id="pwd-current" autocomplete="current-password" />
<button type="button" class="password-field__toggle" id="pwd-current-toggle" aria-label="Mostrar senha"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7Z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3"/></svg></button>
</div>
</div>
<div class="modal-field">
<label for="pwd-new">Nova senha</label>
<div class="password-field">
<input type="password" id="pwd-new" autocomplete="new-password" />
<button type="button" class="password-field__toggle" id="pwd-new-toggle" aria-label="Mostrar senha"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7Z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3"/></svg></button>
</div>
</div>
<div class="modal-field">
<label for="pwd-confirm">Confirmar nova senha</label>
<div class="password-field">
<input type="password" id="pwd-confirm" autocomplete="new-password" />
<button type="button" class="password-field__toggle" id="pwd-confirm-toggle" aria-label="Mostrar senha"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7Z" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3"/></svg></button>
</div>
</div>
<p class="modal-error" id="pwd-error"></p>
<div class="modal-actions">
<button type="button" class="btn-outline" id="pwd-cancel-btn">Cancelar</button>
<button type="button" class="btn-solid" id="pwd-save-btn">Salvar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="notif-history-modal" hidden>
<div class="modal-card">
<h2 class="modal-card__title">Histórico de Notificações</h2>
<ul class="notif-list" id="notif-history-list"></ul>
<div class="modal-actions">
<button type="button" class="btn-outline" id="notif-history-close-btn">Fechar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="calendario-depaula-modal" hidden>
<div class="modal-card iframe-modal-card">
<div class="iframe-modal-header">
<h2 class="modal-card__title">Calendário De Paula</h2>
<button type="button" class="icon-btn" id="calendario-depaula-close-btn" aria-label="Fechar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/></svg>
</button>
</div>
<iframe id="calendario-depaula-iframe" class="iframe-modal-iframe" title="Calendário De Paula"></iframe>
</div>
</div>
<script src="{% static 'js/api.js' %}"></script>
<script src="{% static 'js/confirm-modal.js' %}"></script>
<script src="{% static 'js/sidebar-brand.js' %}"></script>
<script src="{% static 'js/dual-select.js' %}"></script>
<script src="{% static 'js/profiles.js' %}"></script>
<script src="{% static 'js/auth.js' %}"></script>
<script src="{% static 'js/access.js' %}"></script>
<script src="{% static 'js/account.js' %}"></script>
<script src="{% static 'js/favorites.js' %}"></script>
<script src="{% static 'js/events.js' %}"></script>
<script src="{% static 'js/sidebar.js' %}"></script>
<script src="{% static 'js/notifications.js' %}"></script>
<script src="{% static 'js/ajuda-aplicacao.js' %}"></script>
<script>
window.PID_IPS_CONFIG = {
appKey: "importacao-plano-saude-de-paula",
endpointImportacoes: "/importacoes-plano-saude-de-paula",
endpointLinhas: "/importacoes-plano-saude-de-paula-linhas",
endpointAuditoria: "/importacoes-plano-saude-de-paula-auditoria",
endpointAlteracoes: "/importacoes-plano-saude-de-paula-alteracoes",
endpointRegrasCusteio: "/regras-custeio-plano-saude-de-paula",
tituloAjuda: "Importação de Plano de Saúde - De Paula",
};
</script>
<script src="{% static 'js/importacao-plano-saude.js' %}"></script>
</body>
</html>

View File

@ -152,6 +152,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem is-active">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem is-active">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -152,6 +152,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -152,6 +152,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -152,6 +152,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -151,6 +151,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -153,6 +153,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -152,6 +152,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>

View File

@ -151,6 +151,7 @@
</button> </button>
<ul class="nav-group__list"> <ul class="nav-group__list">
<li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li> <li data-app="importacao-plano-saude"><a href="importacao-plano-saude.html" class="nav-subitem">Importação de Plano de Saúde</a></li>
<li data-app="importacao-plano-saude-de-paula"><a href="importacao-plano-saude-de-paula.html" class="nav-subitem">Importação de Plano de Saúde - De Paula</a></li>
</ul> </ul>
</li> </li>