Duplicação da Aplicação Importação Plano de Saúde para o RH
This commit is contained in:
parent
51bc04ef6f
commit
0bac4d41f8
12
CLAUDE.md
12
CLAUDE.md
@ -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 migrate
|
||||
python manage.py seed_portal # recria os 8 perfis + usuários gabriel/bruno
|
||||
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
|
||||
|
||||
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".
|
||||
**`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.
|
||||
|
||||
Não há suíte de testes, lint ou build configurados neste projeto.
|
||||
|
||||
### 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
|
||||
|
||||
@ -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.
|
||||
|
||||
**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.
|
||||
|
||||
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.
|
||||
|
||||
@ -22,6 +22,7 @@ PAGINAS_FRONTEND = [
|
||||
"acessos-gerais.html",
|
||||
"ramais.html",
|
||||
"importacao-plano-saude.html",
|
||||
"importacao-plano-saude-de-paula.html",
|
||||
"custo-contratacao.html",
|
||||
"indicador-desempenho.html",
|
||||
"nao-conformidades.html",
|
||||
|
||||
BIN
media/planos_saude_de_paula/operadora/501_Itamed.PDF
Normal file
BIN
media/planos_saude_de_paula/operadora/501_Itamed.PDF
Normal file
Binary file not shown.
@ -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;
|
||||
|
@ -62,6 +62,7 @@ MODULE_APPS = {
|
||||
],
|
||||
"utilitarios": [
|
||||
{"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": [
|
||||
{"key": "questor", "label": "Questor"},
|
||||
|
||||
@ -173,6 +173,20 @@ class Command(BaseCommand):
|
||||
# tem acesso por padrão, liberado manualmente pra outros perfis depois
|
||||
# pela tela de Perfis de Acesso.
|
||||
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
|
||||
# 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
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -959,6 +959,260 @@ class RegraCusteioPlanoSaude(models.Model):
|
||||
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):
|
||||
"""Departamento organizacional usado pelo Indicador de Desempenho — cada
|
||||
um tem seu próprio cadastro de critérios (`IndicadorCriterio`) e
|
||||
|
||||
@ -24,6 +24,10 @@ from .models import (
|
||||
ImportacaoPlanoSaudeAlteracao,
|
||||
ImportacaoPlanoSaudeAuditoria,
|
||||
ImportacaoPlanoSaudeLinha,
|
||||
ImportacaoPlanoSaudeDePaula,
|
||||
ImportacaoPlanoSaudeDePaulaAlteracao,
|
||||
ImportacaoPlanoSaudeDePaulaAuditoria,
|
||||
ImportacaoPlanoSaudeDePaulaLinha,
|
||||
IndicadorApuracao,
|
||||
IndicadorApuracaoColaborador,
|
||||
IndicadorApuracaoEmpresa,
|
||||
@ -44,6 +48,7 @@ from .models import (
|
||||
Ramal,
|
||||
RamalAusencia,
|
||||
RegraCusteioPlanoSaude,
|
||||
RegraCusteioPlanoSaudeDePaula,
|
||||
TelefoneExterno,
|
||||
Usuario,
|
||||
WidgetUsuario,
|
||||
@ -1152,6 +1157,475 @@ class RegraCusteioPlanoSaudeSerializer(serializers.ModelSerializer):
|
||||
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):
|
||||
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)
|
||||
|
||||
@ -39,6 +39,31 @@ router.register(
|
||||
router.register(
|
||||
"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(
|
||||
"indicadores-percentuais-tipo", views.IndicadorPercentualTipoViewSet, basename="indicador-percentual-tipo"
|
||||
)
|
||||
|
||||
@ -46,6 +46,11 @@ from .models import (
|
||||
ImportacaoPlanoSaudeArquivoOperadora,
|
||||
ImportacaoPlanoSaudeAuditoria,
|
||||
ImportacaoPlanoSaudeLinha,
|
||||
ImportacaoPlanoSaudeDePaula,
|
||||
ImportacaoPlanoSaudeDePaulaAlteracao,
|
||||
ImportacaoPlanoSaudeDePaulaArquivoOperadora,
|
||||
ImportacaoPlanoSaudeDePaulaAuditoria,
|
||||
ImportacaoPlanoSaudeDePaulaLinha,
|
||||
IndicadorApuracao,
|
||||
IndicadorApuracaoColaborador,
|
||||
IndicadorApuracaoEmpresa,
|
||||
@ -66,9 +71,11 @@ from .models import (
|
||||
Ramal,
|
||||
RamalAusencia,
|
||||
RegraCusteioPlanoSaude,
|
||||
RegraCusteioPlanoSaudeDePaula,
|
||||
TelefoneExterno,
|
||||
Usuario,
|
||||
VinculoNomeOperadora,
|
||||
VinculoNomeOperadoraDePaula,
|
||||
WidgetUsuario,
|
||||
)
|
||||
from .indicadores import calculo as indicadores_calculo
|
||||
@ -106,6 +113,13 @@ from .serializers import (
|
||||
ImportacaoPlanoSaudeLinhaCreateSerializer,
|
||||
ImportacaoPlanoSaudeLinhaSerializer,
|
||||
ImportacaoPlanoSaudeListSerializer,
|
||||
ImportacaoPlanoSaudeDePaulaAlteracaoSerializer,
|
||||
ImportacaoPlanoSaudeDePaulaAuditoriaSerializer,
|
||||
ImportacaoPlanoSaudeDePaulaCreateSerializer,
|
||||
ImportacaoPlanoSaudeDePaulaDetailSerializer,
|
||||
ImportacaoPlanoSaudeDePaulaLinhaCreateSerializer,
|
||||
ImportacaoPlanoSaudeDePaulaLinhaSerializer,
|
||||
ImportacaoPlanoSaudeDePaulaListSerializer,
|
||||
IndicadorApuracaoAjusteDepartamentoSerializer,
|
||||
IndicadorApuracaoAjusteGrupoSerializer,
|
||||
IndicadorApuracaoAjusteHonorarioEmpresaSerializer,
|
||||
@ -141,6 +155,7 @@ from .serializers import (
|
||||
RamalAusenciaSerializer,
|
||||
RamalSerializer,
|
||||
RegraCusteioPlanoSaudeSerializer,
|
||||
RegraCusteioPlanoSaudeDePaulaSerializer,
|
||||
SimulacaoCustoContratacaoEntradaSerializer,
|
||||
TelefoneExternoSerializer,
|
||||
UsuarioListSerializer,
|
||||
@ -1496,6 +1511,543 @@ class RegraCusteioPlanoSaudeViewSet(viewsets.ModelViewSet):
|
||||
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):
|
||||
"""Cadastro dos percentuais individual/grupo/departamento por tipo de
|
||||
colaborador (ver IndicadorPercentualTipo em models.py) — nunca editado
|
||||
|
||||
@ -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 = {
|
||||
VALOR_NEGATIVO: "Valor negativo no mês",
|
||||
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;
|
||||
|
||||
async function pidFetchImportacoesPlanoSaude() {
|
||||
return (await pidApiRequest("/importacoes-plano-saude/")) || [];
|
||||
return (await pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/`)) || [];
|
||||
}
|
||||
|
||||
async function pidFetchImportacaoPlanoSaude(id) {
|
||||
return pidApiRequest(`/importacoes-plano-saude/${id}/`);
|
||||
return pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/${id}/`);
|
||||
}
|
||||
|
||||
async function pidFetchOperadorasPlanoSaude() {
|
||||
return (await pidApiRequest("/importacoes-plano-saude/operadoras/")) || [];
|
||||
return (await pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/operadoras/`)) || [];
|
||||
}
|
||||
|
||||
async function pidFetchRegrasEmpresaPlanoSaude() {
|
||||
return (await pidApiRequest("/importacoes-plano-saude/regras-empresa/")) || [];
|
||||
return (await pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/regras-empresa/`)) || [];
|
||||
}
|
||||
|
||||
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) {
|
||||
return pidApiRequest("/importacoes-plano-saude/", { method: "POST", body: formData });
|
||||
return pidApiRequest(`${PID_IPS_CONFIG.endpointImportacoes}/`, { method: "POST", body: formData });
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -135,53 +153,55 @@ async function pidValidarArquivoPlanoSaude(tipo, arquivo, operadoraKey) {
|
||||
formData.append("tipo", tipo);
|
||||
formData.append("arquivo", arquivo);
|
||||
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) {
|
||||
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) {
|
||||
return pidApiRequest("/importacoes-plano-saude-linhas/", {
|
||||
return pidApiRequest(`${PID_IPS_CONFIG.endpointLinhas}/`, {
|
||||
method: "POST",
|
||||
body: { importacao: importacaoId, tipo_lancamento: tipoLancamento },
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
return pidApiRequest(`/importacoes-plano-saude-auditoria/${itemId}/resolver/`, {
|
||||
return pidApiRequest(`${PID_IPS_CONFIG.endpointAuditoria}/${itemId}/resolver/`, {
|
||||
method: "POST",
|
||||
body: { linha_id: linhaId },
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
return (await pidApiRequest("/regras-custeio-plano-saude/")) || [];
|
||||
return (await pidApiRequest(`${PID_IPS_CONFIG.endpointRegrasCusteio}/`)) || [];
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
return pidApiRequest(`/regras-custeio-plano-saude/${id}/`, { method: "DELETE" });
|
||||
return pidApiRequest(`${PID_IPS_CONFIG.endpointRegrasCusteio}/${id}/`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
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) {
|
||||
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",
|
||||
credentials: "include",
|
||||
headers: { "X-CSRFToken": pidGetCookie("csrftoken") || "" },
|
||||
@ -230,7 +250,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
if (!me) return;
|
||||
|
||||
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 contentEl = document.getElementById("ips-content");
|
||||
@ -238,7 +258,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
if (contentEl) contentEl.hidden = !canUse;
|
||||
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 reviewView = document.getElementById("ips-review-view");
|
||||
|
||||
@ -151,6 +151,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
@ -152,6 +152,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
@ -151,6 +151,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
890
templates/importacao-plano-saude-de-paula.html
Normal file
890
templates/importacao-plano-saude-de-paula.html
Normal 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 & 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 & 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&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&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&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&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&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>
|
||||
@ -152,6 +152,7 @@
|
||||
</button>
|
||||
<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-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>
|
||||
</li>
|
||||
|
||||
|
||||
@ -152,6 +152,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
@ -152,6 +152,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
@ -152,6 +152,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
@ -151,6 +151,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
@ -153,6 +153,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
@ -152,6 +152,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
@ -151,6 +151,7 @@
|
||||
</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">Importação de Plano de Saúde - De Paula</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user