Implementação das regras da Tecnomyl
This commit is contained in:
parent
ca9f53837c
commit
39ea801899
@ -71,7 +71,16 @@
|
|||||||
"Bash(curl -s http://127.0.0.1:8000/static/css/halloween-cobweb.css)",
|
"Bash(curl -s http://127.0.0.1:8000/static/css/halloween-cobweb.css)",
|
||||||
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" _scratch_insert_seasonal_toggle.py)",
|
"Bash(\"/c/Users/Depaula/Documents/Portal/.venv/Scripts/python.exe\" _scratch_insert_seasonal_toggle.py)",
|
||||||
"Bash(rm _scratch_insert_seasonal_toggle.py)",
|
"Bash(rm _scratch_insert_seasonal_toggle.py)",
|
||||||
"Bash(sort -t' ' -k2 -n -u)"
|
"Bash(sort -t' ' -k2 -n -u)",
|
||||||
|
"Bash(./.venv/Scripts/python.exe -c ' *)",
|
||||||
|
"Bash(\"./.venv/Scripts/python.exe\" \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\75800899-5860-4fc9-a89a-41027f847563\\\\scratchpad\\\\check_tecnomyl.py\")",
|
||||||
|
"Bash(\"./.venv/Scripts/python.exe\" -c \"import xlrd; print\\(xlrd.__version__\\)\")",
|
||||||
|
"Bash(\"./.venv/Scripts/pip.exe\" list *)",
|
||||||
|
"Bash(\"./.venv/Scripts/python.exe\" \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\75800899-5860-4fc9-a89a-41027f847563\\\\scratchpad\\\\check_amil_898.py\")",
|
||||||
|
"Bash(\"./.venv/Scripts/python.exe\" \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\75800899-5860-4fc9-a89a-41027f847563\\\\scratchpad\\\\check_amil_pipeline_full.py\")",
|
||||||
|
"Bash(\"./.venv/Scripts/python.exe\" \"C:\\\\Users\\\\Depaula\\\\AppData\\\\Local\\\\Temp\\\\claude\\\\c--Users-Depaula-Documents-Portal\\\\75800899-5860-4fc9-a89a-41027f847563\\\\scratchpad\\\\check_dup_cpf.py\")",
|
||||||
|
"Bash(\"./.venv/Scripts/python.exe\" manage.py makemigrations --check --dry-run)",
|
||||||
|
"Bash(\"./.venv/Scripts/pip.exe\" install *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 6.0.7 on 2026-09-17 17:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('portal_api', '0073_contabil_observacao_chave_conta_descricao'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='importacaoplanosaudedepaulalinha',
|
||||||
|
name='tipo_pessoa',
|
||||||
|
field=models.CharField(blank=True, help_text='Titular/Dependente/Agregado, gravado pelo matcher a partir do arquivo da operadora — usado só por regras de empresa que precisam distinguir dependente de agregado (ex.: Amil/Tecnomyl). Ver planos_saude.modelos.LinhaSistema.', max_length=1, verbose_name='Tipo de pessoa (T/D/A)'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='importacaoplanosaudelinha',
|
||||||
|
name='tipo_pessoa',
|
||||||
|
field=models.CharField(blank=True, help_text='Titular/Dependente/Agregado, gravado pelo matcher a partir do arquivo da operadora — usado só por regras de empresa que precisam distinguir dependente de agregado (ex.: Amil/Tecnomyl). Ver planos_saude.modelos.LinhaSistema.', max_length=1, verbose_name='Tipo de pessoa (T/D/A)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -711,6 +711,16 @@ class ImportacaoPlanoSaudeLinha(models.Model):
|
|||||||
)
|
)
|
||||||
descricao = models.CharField("Descrição", max_length=255, blank=True)
|
descricao = models.CharField("Descrição", max_length=255, blank=True)
|
||||||
ordem = models.PositiveIntegerField("Ordem", default=0)
|
ordem = models.PositiveIntegerField("Ordem", default=0)
|
||||||
|
tipo_pessoa = models.CharField(
|
||||||
|
"Tipo de pessoa (T/D/A)",
|
||||||
|
max_length=1,
|
||||||
|
blank=True,
|
||||||
|
help_text=(
|
||||||
|
"Titular/Dependente/Agregado, gravado pelo matcher a partir do arquivo da "
|
||||||
|
"operadora — usado só por regras de empresa que precisam distinguir "
|
||||||
|
"dependente de agregado (ex.: Amil/Tecnomyl). Ver planos_saude.modelos.LinhaSistema."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Linha de importação de plano de saúde"
|
verbose_name = "Linha de importação de plano de saúde"
|
||||||
@ -1063,6 +1073,16 @@ class ImportacaoPlanoSaudeDePaulaLinha(models.Model):
|
|||||||
)
|
)
|
||||||
descricao = models.CharField("Descrição", max_length=255, blank=True)
|
descricao = models.CharField("Descrição", max_length=255, blank=True)
|
||||||
ordem = models.PositiveIntegerField("Ordem", default=0)
|
ordem = models.PositiveIntegerField("Ordem", default=0)
|
||||||
|
tipo_pessoa = models.CharField(
|
||||||
|
"Tipo de pessoa (T/D/A)",
|
||||||
|
max_length=1,
|
||||||
|
blank=True,
|
||||||
|
help_text=(
|
||||||
|
"Titular/Dependente/Agregado, gravado pelo matcher a partir do arquivo da "
|
||||||
|
"operadora — usado só por regras de empresa que precisam distinguir "
|
||||||
|
"dependente de agregado (ex.: Amil/Tecnomyl). Ver planos_saude.modelos.LinhaSistema."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Linha de importação de plano de saúde - De Paula"
|
verbose_name = "Linha de importação de plano de saúde - De Paula"
|
||||||
|
|||||||
@ -344,3 +344,17 @@ Pedido do usuário (2026-09-16), com print da tela de Revisão mostrando um espa
|
|||||||
- **`renderReviewResumoCusteio(importacao)`** (`importacao-plano-saude.js`, nova) monta o resumo a partir dos campos já presentes na importação (`tipos_lancamento`/`custeio_por_tipo`/`regra_empresa`/`regra_empresa_label`) — **não** reaproveita `renderResumoRegra()`/`regrasEmpresaCache` de "Nova Importação" porque essa cache só é buscada ao abrir os modais de "Nova Importação"/"Cadastro de Regras", ficando vazia quando a Revisão é aberta direto do histórico. Em vez disso, `reviewResumoTextoTipo()` detecta cobertura por "regra especial da empresa" olhando se `custeio_por_tipo[tipo]` veio vazio (sem `titular`/`dependente`) com `regra_empresa` preenchido — o mesmo sinal que `ImportacaoPlanoSaudeCreateSerializer.validate()` já grava pra tipos cobertos por uma regra empresa (ver "zeram custeio_por_tipo[tipo] só pros tipos em regra_empresa_tipos" no `CLAUDE.md` deste pacote) — evitando uma segunda fonte de verdade sobre quais tipos a regra cobre.
|
- **`renderReviewResumoCusteio(importacao)`** (`importacao-plano-saude.js`, nova) monta o resumo a partir dos campos já presentes na importação (`tipos_lancamento`/`custeio_por_tipo`/`regra_empresa`/`regra_empresa_label`) — **não** reaproveita `renderResumoRegra()`/`regrasEmpresaCache` de "Nova Importação" porque essa cache só é buscada ao abrir os modais de "Nova Importação"/"Cadastro de Regras", ficando vazia quando a Revisão é aberta direto do histórico. Em vez disso, `reviewResumoTextoTipo()` detecta cobertura por "regra especial da empresa" olhando se `custeio_por_tipo[tipo]` veio vazio (sem `titular`/`dependente`) com `regra_empresa` preenchido — o mesmo sinal que `ImportacaoPlanoSaudeCreateSerializer.validate()` já grava pra tipos cobertos por uma regra empresa (ver "zeram custeio_por_tipo[tipo] só pros tipos em regra_empresa_tipos" no `CLAUDE.md` deste pacote) — evitando uma segunda fonte de verdade sobre quais tipos a regra cobre.
|
||||||
- `abrirRevisao()` chama essa função e decide se o bloco inteiro fica visível (`temResumo || temObs`) — antes só aparecia com observação cadastrada, agora aparece por padrão em toda importação com tipo de lançamento selecionado.
|
- `abrirRevisao()` chama essa função e decide se o bloco inteiro fica visível (`temResumo || temObs`) — antes só aparecia com observação cadastrada, agora aparece por padrão em toda importação com tipo de lançamento selecionado.
|
||||||
- Sem migração, sem endpoint novo — só leitura de campos que a Revisão já carregava.
|
- Sem migração, sem endpoint novo — só leitura de campos que a Revisão já carregava.
|
||||||
|
|
||||||
|
### Rodada 132 — Regra de empresa da Amil (898) na Tecnomyl + suporte a Excel + `tipo_pessoa`
|
||||||
|
|
||||||
|
Depois de validar a regra da Unimed já cadastrada pra Tecnomyl (empresa 1778, ver `CHANGELOG.md` de rodadas anteriores/`CLAUDE.md`), o usuário passou a regra da segunda operadora, Amil Odonto (898): a empresa custeia 100% da mensalidade de titular e dependente direto; agregados (avós, tios, sobrinhos, sogros) pagam 100%, no mesmo valor por pessoa. O arquivo real fornecido (`Demonstrativo de faturamento Tecnomyl 08.2026.xlsx`, competência 08/2026) revelou dois problemas que precisaram ser resolvidos antes da regra em si fazer sentido:
|
||||||
|
|
||||||
|
1. **O arquivo real é Excel, não PDF** — o parser Amil Odonto só lia PDF até aqui. `AmilOdontoMensalidade.extrai()` (`operadoras/amil/odonto_mensalidade.py`) passou a detectar a extensão (`.xlsx`/`.xlsm` → novo caminho via `openpyxl`; qualquer outra → o caminho PDF já existente, inalterado), mesmo padrão de detecção por formato já usado pela Unimed Saúde. `_localiza_cabecalho_xlsx` acha a linha de cabeçalho pelo NOME das colunas (`Código`/`Beneficiário`/`CPF`/`Tipo`/`Mensalidade`), não por posição fixa, já que há um número variável de linhas de título/contrato/fatura acima da tabela real. Validado rodando `extrai()` de ponta a ponta contra o arquivo real: 475 beneficiários (217 T/220 D/38 A), cada um pagando um valor fixo por plano (R$ 20,81 no "DENTAL E200 R PJ_PROT", R$ 16,01 no "DENTAL 200 R PJ_DOC") — e a coluna "Tipo" do relatório batendo 1:1 com os exemplos de "agregado" repassados pelo cliente (avós, sogros, sobrinhos, pai/mãe, irmãos = 'A'; cônjuge/filho(a) = 'D').
|
||||||
|
2. **A planilha padrão do sistema não distingue dependente de agregado** — só sabe titular/não-titular (via `nome_dependente`/`cpf_dependente` vazios ou não), então a regra de custeio (que precisa diferenciar 'D' de 'A' por pessoa) não tinha de onde ler essa informação numa `LinhaSistema`/`ImportacaoPlanoSaudeLinha` já casada. Resolvido com um campo novo, `tipo_pessoa` ('T'/'D'/'A'):
|
||||||
|
- `LinhaSistema.tipo_pessoa` (`modelos.py`, dataclass, default `""`).
|
||||||
|
- `ImportacaoPlanoSaudeLinha.tipo_pessoa`/`ImportacaoPlanoSaudeDePaulaLinha.tipo_pessoa` (`CharField(max_length=1, blank=True)`, migração `0074`, aditiva — sem backfill necessário, linhas antigas ficam em branco e continuam funcionando normalmente, já que só a regra da Amil/Tecnomyl lê esse campo).
|
||||||
|
- `matcher.py` grava esse campo **sempre** no momento do casamento (não só quando há regra de empresa ativa), a partir do `Individuo.tipo` do arquivo da operadora — `_casa_por_cpf`: `linha_destino.tipo_pessoa = ind.tipo`; `_casa_por_nome`: `linha_titular.tipo_pessoa = "T"` / `linha_dep.tipo_pessoa = m.tipo`.
|
||||||
|
- `views.py` (`ImportacaoPlanoSaudeAuditoriaViewSet.resolver`/`...DePaulaAuditoriaViewSet.resolver`) também grava `linha.tipo_pessoa = item.tipo` antes de recalcular a família ao "Vincular pessoa" manualmente — essa linha nunca passou pelo casamento automático, então sem isso cairia sempre no fallback "D" (tratando um agregado vinculado à mão como se fosse dependente). `tipo_pessoa` também entrou em `PLANO_SAUDE_CAMPOS_ALTERACAO` (snapshot de `ImportacaoPlanoSaudeAlteracao.dados_linha`), pra uma exclusão revertida recriar a linha com o `tipo_pessoa` correto.
|
||||||
|
3. **`_regra_amil_898_tecnomyl`** (`regras_empresa.py`, registrada como `amil_898_tecnomyl`): diferente da Tecnomyl-Unimed (teto por família), é uma regra **por pessoa**, sem interação entre membros da família — lê `linha.tipo_pessoa` (fallback "D" se em branco) e manda 100% pra `valor_empresa` (T/D) ou 100% pra `valor` (A). `chave_casamento="cpf"`, `tipos_lancamento=("mensalidade",)`. Validado com o pipeline completo (`processa_importacao` ponta a ponta, com uma planilha padrão sintética) além do teste isolado da função — nenhum erro na divisão titular/dependente vs. agregado.
|
||||||
|
|
||||||
|
**Nota operacional pra próxima competência**: alguns dependentes do arquivo real vêm sem CPF (crianças, principalmente) — normalizado pra `"00000000000"`. Como a estratégia "cpf" (`_casa_por_cpf`) não aceita `vinculos_por_nome` (só a estratégia "nome" reaplica automaticamente um vínculo salvo), uma dessas pessoas vinculada manualmente via "Vincular pessoa" precisa ser vinculada de novo toda competência — comportamento pré-existente de qualquer operadora "cpf" (Amil, SulAmérica, MetLife), não introduzido por esta regra. Ainda não cadastrado em `RegraCusteioPlanoSaude` (banco de produção) — cadastro fica a cargo do usuário direto pela tela "Cadastro de Regras", não é feito por script/ORM neste ambiente.
|
||||||
|
|||||||
@ -20,7 +20,7 @@ portal_api/planos_saude/
|
|||||||
├── unimed_oeste_pr/saude.py Unimed Oeste do Paraná (PDF via pdfplumber, mensalidade+coparticipação por texto da descrição, casamento por nome)
|
├── unimed_oeste_pr/saude.py Unimed Oeste do Paraná (PDF via pdfplumber, mensalidade+coparticipação por texto da descrição, casamento por nome)
|
||||||
├── bradesco/saude.py Bradesco Saúde (PDF **sem texto selecionável** — OCR via `docling`, mensalidade+coparticipação, casamento por nome)
|
├── bradesco/saude.py Bradesco Saúde (PDF **sem texto selecionável** — OCR via `docling`, mensalidade+coparticipação, casamento por nome)
|
||||||
├── bradesco/odonto_mensalidade.py Bradesco Dental/Bradesaude Odonto — 3759 (PDF via pdfplumber, mensalidade+coparticipação, casamento por nome, ver nota abaixo)
|
├── bradesco/odonto_mensalidade.py Bradesco Dental/Bradesaude Odonto — 3759 (PDF via pdfplumber, mensalidade+coparticipação, casamento por nome, ver nota abaixo)
|
||||||
├── amil/odonto_mensalidade.py Amil Odonto — 3758 e 898, dois cadastros da mesma operadora no Questor (PDF via pdfplumber, só mensalidade, casamento por CPF, ver nota abaixo)
|
├── amil/odonto_mensalidade.py Amil Odonto — 3758 e 898, dois cadastros da mesma operadora no Questor (PDF via pdfplumber **ou** Excel via openpyxl — detectado pela extensão do arquivo, ver nota abaixo —, só mensalidade, casamento por CPF)
|
||||||
├── unimed_vitoria/saude.py Unimed Vitória — 4750 (2 PDFs sempre separados, mensalidade+coparticipação, casamento por nome, ver nota abaixo)
|
├── unimed_vitoria/saude.py Unimed Vitória — 4750 (2 PDFs sempre separados, mensalidade+coparticipação, casamento por nome, ver nota abaixo)
|
||||||
├── sulamerica/odonto_mensalidade.py SulAmérica Odonto — 4726 (PDF via pdfplumber, só mensalidade, casamento por CPF, ver nota abaixo)
|
├── sulamerica/odonto_mensalidade.py SulAmérica Odonto — 4726 (PDF via pdfplumber, só mensalidade, casamento por CPF, ver nota abaixo)
|
||||||
├── sulamerica/saude.py SulAmérica Saúde — 5775 (Ottimizza; .xlsx via openpyxl, mensalidade+coparticipação, casamento por CPF, custeio decidido pela "Regra empresa" 1889 - SulAmérica, não pelo parser — ver nota abaixo)
|
├── sulamerica/saude.py SulAmérica Saúde — 5775 (Ottimizza; .xlsx via openpyxl, mensalidade+coparticipação, casamento por CPF, custeio decidido pela "Regra empresa" 1889 - SulAmérica, não pelo parser — ver nota abaixo)
|
||||||
@ -41,6 +41,10 @@ Pra adicionar uma operadora nova: criar `operadoras/<nome>/<arquivo>.py` impleme
|
|||||||
|
|
||||||
**Amil Odonto tem um segundo cadastro no Questor, código 898** — pedido explícito do usuário: a mesma operadora/mesmo layout de arquivo está cadastrada duas vezes no Questor, com `codigo_operadora` diferentes (`CODIGOOUTEMP`), porque empresas distintas usam um ou outro cadastro. `pipeline.OPERADORAS` ganhou uma segunda chave, `amil_odonto_mensalidade_898` (`codigo_operadora="898"`, mesma `nome="Amil Odonto"`), reaproveitando a **mesma classe** `AmilOdontoMensalidade` — regras de extração e de custeio são idênticas às de `amil_odonto_mensalidade`/3758, só muda qual código filtra a planilha padrão buscada no Questor (`busca_linhas_questor`, ver "Planilha padrão via Questor (SQL)" abaixo) e qual aparece no combobox/label ("3758 - Amil Odonto" vs "898 - Amil Odonto"). Ao cadastrar uma regra de custeio (Cadastro de Regras) pra uma empresa que usa o cadastro 898, escolher explicitamente essa entrada no combobox de Operadora, não a de 3758. Se outra operadora aparecer duplicada no Questor do mesmo jeito, replicar este padrão: uma chave por código, mesma classe de `parser`.
|
**Amil Odonto tem um segundo cadastro no Questor, código 898** — pedido explícito do usuário: a mesma operadora/mesmo layout de arquivo está cadastrada duas vezes no Questor, com `codigo_operadora` diferentes (`CODIGOOUTEMP`), porque empresas distintas usam um ou outro cadastro. `pipeline.OPERADORAS` ganhou uma segunda chave, `amil_odonto_mensalidade_898` (`codigo_operadora="898"`, mesma `nome="Amil Odonto"`), reaproveitando a **mesma classe** `AmilOdontoMensalidade` — regras de extração e de custeio são idênticas às de `amil_odonto_mensalidade`/3758, só muda qual código filtra a planilha padrão buscada no Questor (`busca_linhas_questor`, ver "Planilha padrão via Questor (SQL)" abaixo) e qual aparece no combobox/label ("3758 - Amil Odonto" vs "898 - Amil Odonto"). Ao cadastrar uma regra de custeio (Cadastro de Regras) pra uma empresa que usa o cadastro 898, escolher explicitamente essa entrada no combobox de Operadora, não a de 3758. Se outra operadora aparecer duplicada no Questor do mesmo jeito, replicar este padrão: uma chave por código, mesma classe de `parser`.
|
||||||
|
|
||||||
|
**Amil Odonto (898) também lê Excel, não só PDF** — o cliente Tecnomyl (empresa 1778, cadastro 898) manda o mesmo relatório "Demonstrativo Analítico Faturamento" em `.xlsx`, não em PDF. `AmilOdontoMensalidade.extrai()` detecta a extensão do arquivo (`.xlsx`/`.xlsm` → `_extrai_xlsx`, via `openpyxl`; qualquer outra → o caminho PDF já existente, inalterado) — mesmo padrão de detecção por formato já usado pela Unimed Saúde (CSV/PDF). `_localiza_cabecalho_xlsx` acha a linha de cabeçalho de verdade (`Código`/`Beneficiário`/`CPF`/`Tipo`/`Mensalidade`) procurando pelo NOME das colunas dentro das primeiras 20 linhas, não por um número de linha fixo — o arquivo real tem um número variável de linhas de título/contrato/fatura acima da tabela. "Total Família" é só um subtotal por família (mesmo espírito do "Valor Total" do PDF, item 1 acima — ignorado); o valor de cada linha vem de "Mensalidade", somado por `Código` (um mesmo beneficiário pode se repetir em várias linhas — rubricas normais + devoluções/exclusões retroativas de competências anteriores cobradas na mesma fatura, mesma regra geral de "somar por indivíduo"). A coluna "Tipo" já vem como 'T'/'D'/'A' direto do relatório, sem precisar de nenhuma inferência a partir da "Dependência" (livre, só informativa). CPF normalizado + `.zfill(11)` por segurança (mesmo cuidado da MetLife). Validado rodando `extrai()` de ponta a ponta contra o arquivo real da competência 08/2026 (contrato Tecnomyl): 475 beneficiários (217 titulares/220 dependentes/38 agregados), cada um pagando um valor fixo por plano (R$ 20,81 no plano "DENTAL E200 R PJ_PROT", R$ 16,01 no "DENTAL 200 R PJ_DOC") — bate exatamente com a régua "Tipo" T/D/A do relatório real batendo 1:1 com os exemplos de "agregado" (avós, tios, sobrinhos, sogros, pai/mãe, irmãos) repassados pelo cliente pra `amil_898_tecnomyl` (ver "Regra empresa" abaixo). **2 beneficiários com valor final negativo** no arquivo real (exclusão retroativa de competência anterior cobrada nesta fatura) corretamente caem em auditoria (`VALOR_NEGATIVO`), sem relação com o parser em si — comportamento já existente do `matcher.py`, não precisou de nenhum tratamento novo.
|
||||||
|
|
||||||
|
**Nota operacional (não é bug, mas vale saber)**: alguns dependentes do arquivo real vêm sem CPF (campo vazio) — normalizado para `"00000000000"`. Como `chave_casamento="cpf"` pra esta operadora, esse tipo de linha só casa automaticamente se a planilha padrão do Questor também tiver um CPF preenchido pra essa pessoa (o caso normal); se o Questor tiver o CPF real da pessoa (diferente de zeros), a linha cai em auditoria "CPF não encontrado" e precisa de "Vincular pessoa" manual. Diferente da estratégia "nome" (`_casa_por_nome`), a estratégia "cpf" (`_casa_por_cpf`) **não** aceita `vinculos_por_nome` — um vínculo salvo ao resolver manualmente esse caso não é reaplicado automaticamente numa competência futura (a mesma pessoa sem CPF precisaria ser vinculada de novo todo mês). Isso não é uma limitação nova desta rodada, é um comportamento pré-existente de qualquer operadora com `chave_casamento="cpf"` (Amil, SulAmérica, MetLife) — só ficou mais visível aqui porque o arquivo real da Tecnomyl tem vários dependentes menores de idade sem CPF cadastrado. Estender o DE/PARA por nome pra também cobrir a estratégia "cpf" seria uma mudança maior, fora do escopo desta rodada — avaliar se vale a pena caso isso vire um incômodo recorrente.
|
||||||
|
|
||||||
**Unimed Vitória (4750)** — sempre 2 PDFs separados (nunca detecta "tipo de documento" escolhido pelo usuário, detecção automática pelo conteúdo, mesmo espírito da Unimed do Paraná): "Demonstrativo Analítico de Pré Pagamento" (mensalidade) e "Extrato de Co-Participação" (coparticipação), nenhum dos dois com CPF (casamento por nome). Validado contra os dois arquivos reais do cliente Weitnauer Brasil (`pdfplumber` rodou de fato, batendo com os valores impressos no próprio relatório — R$ 340,74 de mensalidade, R$ 55,57 de coparticipação — e casando certo contra a planilha padrão real da empresa 792). Particularidade de extração: a coluna de nome do relatório de mensalidade quebra em duas linhas físicas quando o nome é longo, misturada com a linha de dados num `top` próximo mas não igual — nem `extract_text()` nem `extract_text(layout=True)` resolvem isso sem ambiguidade, então o parser reconstrói as linhas a partir de `extract_words(extra_attrs=["fontname","size"])` agrupadas por posição vertical, e usa sempre o cabeçalho em negrito (nome completo, sem quebra) como fonte do nome, nunca a linha de dados quebrada; a coparticipação não tem espaço literal nenhum entre colunas (todo espaçamento é por posição, não por caractere), o que também exige `extract_words()` em vez de concatenar `page.chars` direto. **Titular/dependente é uma suposição não validada**: nenhum dos dois relatórios traz um marcador textual "Titular"/"Dependente" explícito, e os dois arquivos de exemplo só têm titular, sem nenhum dependente — a classificação usada (sequência "00" da carteirinha "<regional>.<empresa+contrato>.<sequência>-<dv>" = titular, qualquer outra = dependente) é a convenção nacional já conhecida de outras Unimeds, mas nunca confirmada contra um arquivo real desta operadora com dependente; testar com um caso real antes de confiar nela — a validação prévia ("Selecionar arquivo") não pega esse tipo de erro, já que a extração não falha, só classificaria errado.
|
**Unimed Vitória (4750)** — sempre 2 PDFs separados (nunca detecta "tipo de documento" escolhido pelo usuário, detecção automática pelo conteúdo, mesmo espírito da Unimed do Paraná): "Demonstrativo Analítico de Pré Pagamento" (mensalidade) e "Extrato de Co-Participação" (coparticipação), nenhum dos dois com CPF (casamento por nome). Validado contra os dois arquivos reais do cliente Weitnauer Brasil (`pdfplumber` rodou de fato, batendo com os valores impressos no próprio relatório — R$ 340,74 de mensalidade, R$ 55,57 de coparticipação — e casando certo contra a planilha padrão real da empresa 792). Particularidade de extração: a coluna de nome do relatório de mensalidade quebra em duas linhas físicas quando o nome é longo, misturada com a linha de dados num `top` próximo mas não igual — nem `extract_text()` nem `extract_text(layout=True)` resolvem isso sem ambiguidade, então o parser reconstrói as linhas a partir de `extract_words(extra_attrs=["fontname","size"])` agrupadas por posição vertical, e usa sempre o cabeçalho em negrito (nome completo, sem quebra) como fonte do nome, nunca a linha de dados quebrada; a coparticipação não tem espaço literal nenhum entre colunas (todo espaçamento é por posição, não por caractere), o que também exige `extract_words()` em vez de concatenar `page.chars` direto. **Titular/dependente é uma suposição não validada**: nenhum dos dois relatórios traz um marcador textual "Titular"/"Dependente" explícito, e os dois arquivos de exemplo só têm titular, sem nenhum dependente — a classificação usada (sequência "00" da carteirinha "<regional>.<empresa+contrato>.<sequência>-<dv>" = titular, qualquer outra = dependente) é a convenção nacional já conhecida de outras Unimeds, mas nunca confirmada contra um arquivo real desta operadora com dependente; testar com um caso real antes de confiar nela — a validação prévia ("Selecionar arquivo") não pega esse tipo de erro, já que a extração não falha, só classificaria errado.
|
||||||
|
|
||||||
**SulAmérica Odonto (4726)** — um único PDF, sem coparticipação (relatório "Conferência de Faturamento PJ (Completo)" do sistema "IS Odonto" da própria operadora — é só uma cobrança fixa periódica por beneficiário, não há linha de serviço/atendimento nenhuma). Ao contrário das Unimeds, este relatório traz **CPF de todo mundo** (casamento por CPF, mais seguro) e um campo textual explícito de "Grau parentesco" (TITULAR/CONJUGE/OUTROS/DEP PERMANENTE/...) — nenhuma suposição sobre numeração de carteirinha foi necessária aqui. Validado rodando `pdfplumber` e o `pipeline.processa_importacao` completo contra o arquivo real (15 beneficiários, R$ 437,40 no total — bate exatamente com "Total R$ 437,40" impresso no relatório) e a planilha padrão real da empresa 792 (13 dos 15 beneficiários casaram certo por CPF; os 2 ausentes da planilha de teste foram corretamente para auditoria "CPF não encontrado", não ignorados). **Cada família tem um "totalizador" impresso ao final** (ex.: "R$ 87,48" somando os 3 beneficiários de uma família) — por pedido explícito do usuário, esse total **nunca é usado**: o lançamento é sempre feito pela coluna "Valor" de cada linha de beneficiário individual (R$ 29,16 no exemplo), a mesma lógica de "usar o valor por linha, ignorar o subtotal impresso" já aplicada à Unimed do Paraná/Vitória. Particularidade de extração: quando o nome de um beneficiário (ou da família, no cabeçalho) ultrapassa a largura da coluna, o próprio relatório **corta o texto sem reticências e sem terminar de completar a última palavra** (ex.: "DANIELE MARTINS FERREIRA DA SILVA" sai como "DANIELE MARTINS FERREIRA DA" + "SILV" cortado, perdendo o "A" final) — como o casamento é por CPF, isso nunca afeta a correção do lançamento (nome é só exibição), então o parser não tenta reconstruir o nome quebrado, só usa a primeira linha física de cada beneficiário (onde já estão código/CPF/data nascimento/grau/valor completos, nenhum desses quebra, só o nome às vezes).
|
**SulAmérica Odonto (4726)** — um único PDF, sem coparticipação (relatório "Conferência de Faturamento PJ (Completo)" do sistema "IS Odonto" da própria operadora — é só uma cobrança fixa periódica por beneficiário, não há linha de serviço/atendimento nenhuma). Ao contrário das Unimeds, este relatório traz **CPF de todo mundo** (casamento por CPF, mais seguro) e um campo textual explícito de "Grau parentesco" (TITULAR/CONJUGE/OUTROS/DEP PERMANENTE/...) — nenhuma suposição sobre numeração de carteirinha foi necessária aqui. Validado rodando `pdfplumber` e o `pipeline.processa_importacao` completo contra o arquivo real (15 beneficiários, R$ 437,40 no total — bate exatamente com "Total R$ 437,40" impresso no relatório) e a planilha padrão real da empresa 792 (13 dos 15 beneficiários casaram certo por CPF; os 2 ausentes da planilha de teste foram corretamente para auditoria "CPF não encontrado", não ignorados). **Cada família tem um "totalizador" impresso ao final** (ex.: "R$ 87,48" somando os 3 beneficiários de uma família) — por pedido explícito do usuário, esse total **nunca é usado**: o lançamento é sempre feito pela coluna "Valor" de cada linha de beneficiário individual (R$ 29,16 no exemplo), a mesma lógica de "usar o valor por linha, ignorar o subtotal impresso" já aplicada à Unimed do Paraná/Vitória. Particularidade de extração: quando o nome de um beneficiário (ou da família, no cabeçalho) ultrapassa a largura da coluna, o próprio relatório **corta o texto sem reticências e sem terminar de completar a última palavra** (ex.: "DANIELE MARTINS FERREIRA DA SILVA" sai como "DANIELE MARTINS FERREIRA DA" + "SILV" cortado, perdendo o "A" final) — como o casamento é por CPF, isso nunca afeta a correção do lançamento (nome é só exibição), então o parser não tenta reconstruir o nome quebrado, só usa a primeira linha física de cada beneficiário (onde já estão código/CPF/data nascimento/grau/valor completos, nenhum desses quebra, só o nome às vezes).
|
||||||
@ -86,6 +90,7 @@ Essa divisão é calculada por `matcher._calcula_valores(valor_total, regra)` (c
|
|||||||
- `ImportacaoPlanoSaudeAuditoria`: espelha `ItemAuditoria` — os campos extraídos do arquivo da operadora (`motivo`/`nome`/`valor`/`detalhe`...) são read-only na tela; `resolvida`/`linha_vinculada` são a exceção, graváveis via a resolução manual (ver "Resolução manual de auditoria por nome" abaixo). `MOTIVOS_RESOLVIVEIS = ("NOME_DIVERGENTE", "NAO_CADASTRADO")` (atributo de classe) é a lista dos dois motivos "de leitura/grafia de nome" que aceitam esse fluxo — `VALOR_NEGATIVO`/`TIPO_INVALIDO` são outra categoria de problema (valor real negativo, tipo de despesa não mapeado) e não têm solução por "essa é a mesma pessoa".
|
- `ImportacaoPlanoSaudeAuditoria`: espelha `ItemAuditoria` — os campos extraídos do arquivo da operadora (`motivo`/`nome`/`valor`/`detalhe`...) são read-only na tela; `resolvida`/`linha_vinculada` são a exceção, graváveis via a resolução manual (ver "Resolução manual de auditoria por nome" abaixo). `MOTIVOS_RESOLVIVEIS = ("NOME_DIVERGENTE", "NAO_CADASTRADO")` (atributo de classe) é a lista dos dois motivos "de leitura/grafia de nome" que aceitam esse fluxo — `VALOR_NEGATIVO`/`TIPO_INVALIDO` são outra categoria de problema (valor real negativo, tipo de despesa não mapeado) e não têm solução por "essa é a mesma pessoa".
|
||||||
- `ImportacaoPlanoSaudeAlteracao`: log de cada edição de campo/inclusão/exclusão de linha feita manualmente na revisão — ver seção "Alterações" abaixo.
|
- `ImportacaoPlanoSaudeAlteracao`: log de cada edição de campo/inclusão/exclusão de linha feita manualmente na revisão — ver seção "Alterações" abaixo.
|
||||||
- `RegraCusteioPlanoSaude`: regra de custeio salva pra reaplicar em importações futuras (ex.: "092 - Unimed") — ver "Regras de custeio salvas" abaixo. Lista compartilhada, mesmo espírito de `LinkFerramenta`/`AcessoGeral` — o próprio model não tem FK pra nada; é `ImportacaoPlanoSaude.regra_custeio_salva` que aponta pra cá (opcional, `SET_NULL`), só como registro de qual regra (se alguma) foi aplicada pra preencher aquele formulário.
|
- `RegraCusteioPlanoSaude`: regra de custeio salva pra reaplicar em importações futuras (ex.: "092 - Unimed") — ver "Regras de custeio salvas" abaixo. Lista compartilhada, mesmo espírito de `LinkFerramenta`/`AcessoGeral` — o próprio model não tem FK pra nada; é `ImportacaoPlanoSaude.regra_custeio_salva` que aponta pra cá (opcional, `SET_NULL`), só como registro de qual regra (se alguma) foi aplicada pra preencher aquele formulário.
|
||||||
|
- `ImportacaoPlanoSaudeLinha`/`ImportacaoPlanoSaudeDePaulaLinha` ganharam `tipo_pessoa` (`CharField(max_length=1, blank=True)`, migração `0074`) — 'T'/'D'/'A' (Titular/Dependente/Agregado), espelhando `LinhaSistema.tipo_pessoa` (`planos_saude/modelos.py`). Não é exposto em nenhum serializer (uso só interno, nenhum consumidor de frontend precisa dele) — existe só pra alimentar uma regra de empresa que precise diferenciar dependente de agregado por PESSOA (a planilha padrão do sistema em si só distingue titular/não-titular, via `nome_dependente`/`cpf_dependente` vazios ou não — nunca soube separar "D" de "A"). Ver "Regra empresa" abaixo (`amil_898_tecnomyl`) pro primeiro consumidor real.
|
||||||
|
|
||||||
## Fluxo e endpoints
|
## Fluxo e endpoints
|
||||||
|
|
||||||
@ -217,7 +222,7 @@ O bloco de checkboxes/radios de custeio (`.ips-tipo-field`, mensalidade/copartic
|
|||||||
|
|
||||||
## Regra empresa (custeio especial por empresa, mensalidade e/ou coparticipação)
|
## Regra empresa (custeio especial por empresa, mensalidade e/ou coparticipação)
|
||||||
|
|
||||||
Cobre regras de custeio negociadas com uma empresa específica que não cabem no desenho normal "por tipo de lançamento × titular/dependente" — por serem calculadas por **família inteira** (titular + dependentes somados, não por pessoa) e/ou por serem um critério fixo (não um percentual/teto configurável). O algoritmo em si (`portal_api/planos_saude/regras_empresa.py`, `REGRAS_EMPRESA: Dict[str, dict]`) continua sendo um registro fixo no código, cadastrado pelo desenvolvedor quando o cliente repassa uma regra nova — a escolha de USAR uma regra especial vive dentro do Cadastro de Regras por empresa+operadora: o campo `RegraCusteioPlanoSaude.regra_empresa_chave` (chave de `REGRAS_EMPRESA`) é configurado uma vez, junto do resto do custeio, no modal "Cadastro de Regras" — "Nova Importação" só resolve o que já foi cadastrado, sem checkbox próprio.
|
Cobre regras de custeio negociadas com uma empresa específica que não cabem no desenho normal "por tipo de lançamento × titular/dependente" — por serem calculadas por **família inteira** (titular + dependentes somados, não por pessoa), por serem um critério fixo (não um percentual/teto configurável) e/ou por dependerem de uma distinção por PESSOA que o sistema não guarda (ex.: dependente vs. agregado — ver `amil_898_tecnomyl`/`tipo_pessoa` abaixo). O algoritmo em si (`portal_api/planos_saude/regras_empresa.py`, `REGRAS_EMPRESA: Dict[str, dict]`) continua sendo um registro fixo no código, cadastrado pelo desenvolvedor quando o cliente repassa uma regra nova — a escolha de USAR uma regra especial vive dentro do Cadastro de Regras por empresa+operadora: o campo `RegraCusteioPlanoSaude.regra_empresa_chave` (chave de `REGRAS_EMPRESA`) é configurado uma vez, junto do resto do custeio, no modal "Cadastro de Regras" — "Nova Importação" só resolve o que já foi cadastrado, sem checkbox próprio.
|
||||||
|
|
||||||
**Deixou de ser exclusivo de "mensalidade"** — cada regra em `REGRAS_EMPRESA` agora declara `tipos_lancamento` (quais tipos ela cobre — a Tecnomyl abaixo só cobre `("mensalidade",)`, a Ottimizza abaixo cobre `("mensalidade", "coparticipacao")`) e `chave_casamento` (que estratégia de casamento a regra exige — "nome" pra regras que precisam agrupar família, "cpf" pra regras por pessoa sem agrupamento). Por isso o checkbox do Cadastro de Regras foi renomeado de "Mensalidade usa regra especial da empresa" pra **"Regra especial da empresa"** (`#ips-form-tipo-regra-empresa`, mesmo id) — decisão explícita do usuário, "considerando que neste lugar trata não apenas mensalidade mas também a coparticipação".
|
**Deixou de ser exclusivo de "mensalidade"** — cada regra em `REGRAS_EMPRESA` agora declara `tipos_lancamento` (quais tipos ela cobre — a Tecnomyl abaixo só cobre `("mensalidade",)`, a Ottimizza abaixo cobre `("mensalidade", "coparticipacao")`) e `chave_casamento` (que estratégia de casamento a regra exige — "nome" pra regras que precisam agrupar família, "cpf" pra regras por pessoa sem agrupamento). Por isso o checkbox do Cadastro de Regras foi renomeado de "Mensalidade usa regra especial da empresa" pra **"Regra especial da empresa"** (`#ips-form-tipo-regra-empresa`, mesmo id) — decisão explícita do usuário, "considerando que neste lugar trata não apenas mensalidade mas também a coparticipação".
|
||||||
|
|
||||||
@ -230,5 +235,10 @@ Cobre regras de custeio negociadas com uma empresa específica que não cabem no
|
|||||||
- **Cálculo é por família, com prioridade explícita: dependentes primeiro, titular absorve o residual** (`regras_empresa._aplica_teto_familia`) — decisão explícita do cliente, e diferente de uma primeira versão (revertida) que distribuía o teto **proporcionalmente** entre todas as linhas. O algoritmo percorre primeiro os dependentes (na ordem em que aparecem no arquivo da operadora), cada um recebendo `valor_empresa = min(seu valor, o que sobrou do teto)`; só depois de todos os dependentes processados o titular absorve o que sobrou do teto (`teto_restante`), com o excedente (se houver) virando desconto do empregado nessa mesma linha. Ex.: família com dependente de R$559,57 e titular de R$314,12 (teto R$661,61) — dependente sai com `valor_empresa=559,57`/`valor=0` (coberto integralmente), sobra `661,61-559,57=102,04` de teto pro titular, que sai com `valor_empresa=102,04`/`valor=212,08`. Se os dependentes sozinhos já consumirem o teto inteiro, o titular fica com `valor_empresa=0` (desconto integral) e, se ainda sobrar dependente sem cobrir depois disso, esse dependente também é parcialmente descontado. Validado rodando o pipeline direto com os dois exemplos passados pelo cliente (família de R$800 → R$661,61 empresa/R$138,39 empregado no total; família abaixo do teto → 100% empresa) e reproduzindo exatamente um caso real reportado pelo usuário (família Caroline Fernandes/Luciano Ramos, R$873,69 no total) depois do ajuste de prioridade.
|
- **Cálculo é por família, com prioridade explícita: dependentes primeiro, titular absorve o residual** (`regras_empresa._aplica_teto_familia`) — decisão explícita do cliente, e diferente de uma primeira versão (revertida) que distribuía o teto **proporcionalmente** entre todas as linhas. O algoritmo percorre primeiro os dependentes (na ordem em que aparecem no arquivo da operadora), cada um recebendo `valor_empresa = min(seu valor, o que sobrou do teto)`; só depois de todos os dependentes processados o titular absorve o que sobrou do teto (`teto_restante`), com o excedente (se houver) virando desconto do empregado nessa mesma linha. Ex.: família com dependente de R$559,57 e titular de R$314,12 (teto R$661,61) — dependente sai com `valor_empresa=559,57`/`valor=0` (coberto integralmente), sobra `661,61-559,57=102,04` de teto pro titular, que sai com `valor_empresa=102,04`/`valor=212,08`. Se os dependentes sozinhos já consumirem o teto inteiro, o titular fica com `valor_empresa=0` (desconto integral) e, se ainda sobrar dependente sem cobrir depois disso, esse dependente também é parcialmente descontado. Validado rodando o pipeline direto com os dois exemplos passados pelo cliente (família de R$800 → R$661,61 empresa/R$138,39 empregado no total; família abaixo do teto → 100% empresa) e reproduzindo exatamente um caso real reportado pelo usuário (família Caroline Fernandes/Luciano Ramos, R$873,69 no total) depois do ajuste de prioridade.
|
||||||
- **`_aplica_teto_familia` é duck-typed de propósito** (`linhas_e_valores: List[Tuple[Any, float]]`, `_eh_linha_titular()` própria em vez de `LinhaSistema.eh_linha_titular()`): roda tanto contra `LinhaSistema` (pipeline, na criação da importação) quanto contra `ImportacaoPlanoSaudeLinha` (model Django, no recálculo pós "Vincular pessoa" — ver `views._recalcula_familia_regra_empresa` e "Resolução manual de auditoria por nome" acima) — as duas classes têm os mesmos atributos de string (`nome_dependente`/`cpf_dependente`/`valor_empresa`/`valor`), só a segunda não tem o método `eh_linha_titular()`.
|
- **`_aplica_teto_familia` é duck-typed de propósito** (`linhas_e_valores: List[Tuple[Any, float]]`, `_eh_linha_titular()` própria em vez de `LinhaSistema.eh_linha_titular()`): roda tanto contra `LinhaSistema` (pipeline, na criação da importação) quanto contra `ImportacaoPlanoSaudeLinha` (model Django, no recálculo pós "Vincular pessoa" — ver `views._recalcula_familia_regra_empresa` e "Resolução manual de auditoria por nome" acima) — as duas classes têm os mesmos atributos de string (`nome_dependente`/`cpf_dependente`/`valor_empresa`/`valor`), só a segunda não tem o método `eh_linha_titular()`.
|
||||||
- **`sulamerica_5775_ottimizza`** — Ottimizza (código 1889) na SulAmérica (5775, ver "SulAmérica Saúde" acima): critério fixo, sem teto/percentual — mensalidade do titular é 100% custeada pela empresa, mensalidade do dependente é 100% descontada do empregado, e toda coparticipação (titular ou dependente) é 100% descontada do empregado. `chave_casamento="cpf"` (o parser já resolve cada indivíduo por CPF, sem precisar agrupar família — `_regra_sulamerica_5775_ottimizza` decide por linha, olhando só `_eh_linha_titular(linha)` e o `tipo_lancamento` recebido), `tipos_lancamento=("mensalidade", "coparticipacao")` — as duas cobertas pela mesma função, que ramifica por `tipo_lancamento`. Reproduz exatamente o padrão observado na planilha real da Ottimizza (toda linha de titular só vem com "Benefício Mensalidade" preenchido, toda linha de dependente só com "Desconto Mensalidade", "Benefício Coparticipação" nunca preenchido) — confirmado rodando `pipeline.processa_importacao` de ponta a ponta com a regra ativa contra o arquivo real e batendo centavo a centavo com as 4 colunas somadas direto da planilha (R$ 23.722,14 empresa/R$ 1.404,26 empregado de mensalidade; R$ 1.540,84 empregado de coparticipação).
|
- **`sulamerica_5775_ottimizza`** — Ottimizza (código 1889) na SulAmérica (5775, ver "SulAmérica Saúde" acima): critério fixo, sem teto/percentual — mensalidade do titular é 100% custeada pela empresa, mensalidade do dependente é 100% descontada do empregado, e toda coparticipação (titular ou dependente) é 100% descontada do empregado. `chave_casamento="cpf"` (o parser já resolve cada indivíduo por CPF, sem precisar agrupar família — `_regra_sulamerica_5775_ottimizza` decide por linha, olhando só `_eh_linha_titular(linha)` e o `tipo_lancamento` recebido), `tipos_lancamento=("mensalidade", "coparticipacao")` — as duas cobertas pela mesma função, que ramifica por `tipo_lancamento`. Reproduz exatamente o padrão observado na planilha real da Ottimizza (toda linha de titular só vem com "Benefício Mensalidade" preenchido, toda linha de dependente só com "Desconto Mensalidade", "Benefício Coparticipação" nunca preenchido) — confirmado rodando `pipeline.processa_importacao` de ponta a ponta com a regra ativa contra o arquivo real e batendo centavo a centavo com as 4 colunas somadas direto da planilha (R$ 23.722,14 empresa/R$ 1.404,26 empregado de mensalidade; R$ 1.540,84 empregado de coparticipação).
|
||||||
|
- **`amil_898_tecnomyl`** — Amil Odonto (código 898) na Tecnomyl (empresa 1778), repassada pelo cliente em 09/2026: a empresa custeia 100% da mensalidade de titular e dependente direto (cônjuge, filho(a)); agregados (avós, tios, sobrinhos, sogros, pai/mãe, irmãos) têm a mensalidade 100% descontada do empregado, no MESMO valor por pessoa — a regra é só sobre QUEM paga, não sobre um teto/percentual, por isso cobre os dois planos do contrato (E200 a R$ 20,81/pessoa e "DENTAL 200" a R$ 16,01/pessoa) sem precisar de nenhum valor fixo no código. `chave_casamento="cpf"` (o parser já resolve cada indivíduo por CPF), `tipos_lancamento=("mensalidade",)` — Amil Odonto não traz coparticipação (ver docstring do parser).
|
||||||
|
- **Diferente das outras duas regras: não usa família nem `_eh_linha_titular`** — é a primeira regra de empresa que precisa diferenciar dependente de agregado por PESSOA, uma distinção que a planilha padrão do sistema não guarda (só sabe titular/não-titular). Por isso `LinhaSistema`/`ImportacaoPlanoSaudeLinha`/`ImportacaoPlanoSaudeDePaulaLinha` ganharam `tipo_pessoa` ('T'/'D'/'A', ver "Modelos" acima) — gravado **sempre** (não só quando há uma regra de empresa ativa) pelo `matcher.py` no momento do casamento, a partir do `Individuo.tipo` original do arquivo da operadora (`_casa_por_cpf`: `linha_destino.tipo_pessoa = ind.tipo`; `_casa_por_nome`: `linha_titular.tipo_pessoa = "T"` / `linha_dep.tipo_pessoa = m.tipo`). `_regra_amil_898_tecnomyl` só lê `getattr(linha, "tipo_pessoa", "") or "D"` de cada linha (fallback pra "D" — custeada pela empresa — quando em branco, ex.: linha incluída manualmente via "Adicionar linha", que nunca passa pelo casamento automático).
|
||||||
|
- **"Vincular pessoa" (resolução manual de auditoria) também grava `tipo_pessoa`**: como a linha resolvida nunca passou pelo casamento automático (senão não estaria em branco), `ImportacaoPlanoSaudeAuditoriaViewSet.resolver`/`...DePaulaAuditoriaViewSet.resolver` gravam `linha.tipo_pessoa = item.tipo` antes de chamar `_recalcula_familia_regra_empresa(_de_paula)` — sem isso, uma linha vinculada manualmente cairia sempre no fallback "D", tratando um agregado vinculado à mão como se fosse dependente.
|
||||||
|
- **Nota operacional**: dependentes sem CPF no arquivo real da Tecnomyl (comum em crianças) caem em auditoria "CPF não encontrado" se a planilha padrão do Questor tiver o CPF real dessa pessoa — e, ao contrário da estratégia "nome", a estratégia "cpf" não reaplica um vínculo salvo automaticamente numa competência futura (`_casa_por_cpf` não aceita `vinculos_por_nome`), então a mesma pessoa precisa ser vinculada de novo todo mês. Comportamento pré-existente de qualquer operadora "cpf" (Amil, SulAmérica, MetLife), não uma limitação introduzida por esta regra — ver nota em "Formato Excel (898, Tecnomyl)" acima.
|
||||||
|
- Validado rodando `extrai()` + a regra + o pipeline completo (`processa_importacao`) contra o arquivo real da competência 08/2026 — ver detalhe em "Formato Excel (898, Tecnomyl)" acima.
|
||||||
- **Trava de compatibilidade generalizada**: `regras_empresa.valida_regra_empresa()` recusa explicitamente (`RegraEmpresaIncompativelError`, capturada à parte em `views.py` pra devolver a mensagem certa, não o erro genérico de "formato de arquivo") se (a) nenhum tipo selecionado na importação está entre os `tipos_lancamento` da regra, (b) a operadora escolhida não usa a `chave_casamento` que a regra exige pra algum tipo coberto, ou (c) a planilha padrão anexada não tem nenhuma linha com o `codigo_empresa` esperado pela regra — trava contra aplicar a regra de uma empresa a outra por engano (vale pras duas regras, não só pra Tecnomyl como antes).
|
- **Trava de compatibilidade generalizada**: `regras_empresa.valida_regra_empresa()` recusa explicitamente (`RegraEmpresaIncompativelError`, capturada à parte em `views.py` pra devolver a mensagem certa, não o erro genérico de "formato de arquivo") se (a) nenhum tipo selecionado na importação está entre os `tipos_lancamento` da regra, (b) a operadora escolhida não usa a `chave_casamento` que a regra exige pra algum tipo coberto, ou (c) a planilha padrão anexada não tem nenhuma linha com o `codigo_empresa` esperado pela regra — trava contra aplicar a regra de uma empresa a outra por engano (vale pras duas regras, não só pra Tecnomyl como antes).
|
||||||
- **"Vincular pessoa" (resolução manual de auditoria) também generalizada**: `_recalcula_familia_regra_empresa` (views.py) filtra por `linha.tipo_lancamento` (o tipo da própria linha resolvida), não mais fixo em `"mensalidade"`, e passa esse tipo como segundo argumento pra `regra["aplica"]`; `resolver()` decide se aplica esse caminho checando se `item.tipo_lancamento` está em `REGRAS_EMPRESA[chave]["tipos_lancamento"]`, não mais comparando com a string `"mensalidade"` direto.
|
- **"Vincular pessoa" (resolução manual de auditoria) também generalizada**: `_recalcula_familia_regra_empresa` (views.py) filtra por `linha.tipo_lancamento` (o tipo da própria linha resolvida), não mais fixo em `"mensalidade"`, e passa esse tipo como segundo argumento pra `regra["aplica"]`; `resolver()` decide se aplica esse caminho checando se `item.tipo_lancamento` está em `REGRAS_EMPRESA[chave]["tipos_lancamento"]`, não mais comparando com a string `"mensalidade"` direto.
|
||||||
|
|||||||
@ -240,6 +240,12 @@ def _casa_por_cpf(
|
|||||||
auditoria.append(_item_nao_cadastrado(ind, "CPF não encontrado na planilha padrão do sistema."))
|
auditoria.append(_item_nao_cadastrado(ind, "CPF não encontrado na planilha padrão do sistema."))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Sempre gravado (não só quando há regra_empresa) — é a única forma
|
||||||
|
# de uma regra de empresa futura saber, por uma LinhaSistema já
|
||||||
|
# persistida, se ela era um dependente direto ou um agregado no
|
||||||
|
# arquivo original da operadora (ver `LinhaSistema.tipo_pessoa`).
|
||||||
|
linha_destino.tipo_pessoa = ind.tipo
|
||||||
|
|
||||||
if regra_empresa_fn is not None:
|
if regra_empresa_fn is not None:
|
||||||
linhas_e_valores.append((linha_destino, ind.valor_total))
|
linhas_e_valores.append((linha_destino, ind.valor_total))
|
||||||
else:
|
else:
|
||||||
@ -349,6 +355,7 @@ def _casa_por_nome(
|
|||||||
# a partir da planilha, então a chave certa é o nome REAL do
|
# a partir da planilha, então a chave certa é o nome REAL do
|
||||||
# titular na planilha (`linha_titular.nome_func`), não o nome que
|
# titular na planilha (`linha_titular.nome_func`), não o nome que
|
||||||
# veio do arquivo da operadora (que pode ser divergente).
|
# veio do arquivo da operadora (que pode ser divergente).
|
||||||
|
linha_titular.tipo_pessoa = "T" # sempre gravado, ver nota em _casa_por_cpf
|
||||||
dependentes_da_familia = dependentes_por_titular.get(normaliza_nome(linha_titular.nome_func), {})
|
dependentes_da_familia = dependentes_por_titular.get(normaliza_nome(linha_titular.nome_func), {})
|
||||||
linhas_e_valores_familia: List[Tuple[LinhaSistema, float]] = []
|
linhas_e_valores_familia: List[Tuple[LinhaSistema, float]] = []
|
||||||
|
|
||||||
@ -394,6 +401,8 @@ def _casa_por_nome(
|
|||||||
))
|
))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
linha_dep.tipo_pessoa = m.tipo # 'D' ou 'A', ver nota em _casa_por_cpf
|
||||||
|
|
||||||
if regra_empresa_fn is not None:
|
if regra_empresa_fn is not None:
|
||||||
linhas_e_valores_familia.append((linha_dep, m.valor_total))
|
linhas_e_valores_familia.append((linha_dep, m.valor_total))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -84,6 +84,16 @@ class LinhaSistema:
|
|||||||
valor_empresa: str
|
valor_empresa: str
|
||||||
valor: str
|
valor: str
|
||||||
descricao: str
|
descricao: str
|
||||||
|
# 'T'/'D'/'A' (Titular/Dependente/Agregado) — gravado pelo matcher no
|
||||||
|
# momento do casamento, a partir do Individuo.tipo do arquivo da
|
||||||
|
# operadora (ver matcher._casa_por_cpf/_casa_por_nome). A planilha
|
||||||
|
# padrão do sistema em si não distingue dependente de agregado (só
|
||||||
|
# titular/não-titular, via nome_dependente/cpf_dependente) — este campo
|
||||||
|
# existe só pra alimentar regras de empresa que precisam dessa
|
||||||
|
# distinção por pessoa (ex.: portal_api.planos_saude.regras_empresa,
|
||||||
|
# regra da Amil/Tecnomyl). Fica "" quando a linha nunca foi casada
|
||||||
|
# automaticamente (ex.: incluída manualmente via "Adicionar linha").
|
||||||
|
tipo_pessoa: str = ""
|
||||||
|
|
||||||
def cpf_func_normalizado(self) -> str:
|
def cpf_func_normalizado(self) -> str:
|
||||||
return normaliza_cpf(self.cpf_func)
|
return normaliza_cpf(self.cpf_func)
|
||||||
|
|||||||
@ -3,10 +3,36 @@ Amil Odonto - Mensalidade. Porta de
|
|||||||
projects/project/operadoras/amil/odonto_mensalidade.py.
|
projects/project/operadoras/amil/odonto_mensalidade.py.
|
||||||
|
|
||||||
Formato recebido: PDF "Demonstrativo Analítico de Faturamento - Por
|
Formato recebido: PDF "Demonstrativo Analítico de Faturamento - Por
|
||||||
Contrato / Empresa". Esta operadora normalmente não traz coparticipação,
|
Contrato / Empresa" (código 3758) **ou** Excel do mesmo relatório (código
|
||||||
só mensalidade — mas a arquitetura já está pronta para o dia em que
|
898 — ver "Formato Excel (898, Tecnomyl)" abaixo), detectado automaticamente
|
||||||
precisarmos ler coparticipação de outra operadora (basta criar uma classe
|
pela extensão do arquivo (mesmo padrão de detecção por conteúdo/extensão já
|
||||||
irmã, ex: operadoras/amil/odonto_coparticipacao.py).
|
usado por outras operadoras, ex.: Unimed Saúde CSV/PDF). Esta operadora
|
||||||
|
normalmente não traz coparticipação, só mensalidade — mas a arquitetura já
|
||||||
|
está pronta para o dia em que precisarmos ler coparticipação de outra
|
||||||
|
operadora (basta criar uma classe irmã, ex: operadoras/amil/odonto_coparticipacao.py).
|
||||||
|
|
||||||
|
Formato Excel (898, Tecnomyl) — validado contra o arquivo real da
|
||||||
|
competência 08/2026 (509 linhas, contrato Tecnomyl): mesmo relatório
|
||||||
|
"Demonstrativo Analítico Faturamento", só que exportado em `.xlsx` em vez
|
||||||
|
de PDF. O cabeçalho de verdade (`Código`/`Beneficiário`/`Matrícula`/`CPF`/
|
||||||
|
`Plano`/`Tipo`/`Idade`/`Dependência`/.../`Mensalidade`/`Total Família`) não
|
||||||
|
fica sempre na mesma linha (acima dele há linhas de título/contrato/fatura
|
||||||
|
cuja quantidade pode variar) — por isso `_localiza_cabecalho_xlsx` procura a
|
||||||
|
linha certa pelo NOME das colunas em vez de um número de linha fixo.
|
||||||
|
"Total Família" é só um subtotal por família (mesmo espírito do "Valor
|
||||||
|
Total" do PDF — ignorado, ver item 1 abaixo); o valor de cada linha vem da
|
||||||
|
coluna "Mensalidade". Um mesmo `Código` pode se repetir em várias linhas
|
||||||
|
(rubricas normais + devoluções/exclusões retroativas de competências
|
||||||
|
anteriores, cobradas na mesma fatura) — somadas por indivíduo, mesma regra
|
||||||
|
geral do pacote. "Tipo" já vem como 'T'/'D'/'A' direto do relatório
|
||||||
|
(confirmado: 'A' cobre exatamente avós/tios/sobrinhos/sogros/pai-mãe/irmãos
|
||||||
|
— os mesmos exemplos de "agregado" repassados pelo cliente para a regra de
|
||||||
|
custeio, ver `portal_api.planos_saude.regras_empresa`), então não precisa
|
||||||
|
de nenhuma inferência a partir da coluna "Dependência" (livre, só
|
||||||
|
informativa). CPF vem com máscara (pontos/traço) e como texto no arquivo
|
||||||
|
real — normalizado e preenchido com zeros à esquerda por segurança (mesmo
|
||||||
|
cuidado já necessário no relatório da MetLife, ver `operadoras/metlife/
|
||||||
|
odonto_mensalidade.py`).
|
||||||
|
|
||||||
Particularidades deste relatório (descobertas inspecionando o PDF real):
|
Particularidades deste relatório (descobertas inspecionando o PDF real):
|
||||||
|
|
||||||
@ -42,11 +68,12 @@ agregados), R$ 1.630,93 no total — bate exatamente com os totais impressos
|
|||||||
no próprio relatório.
|
no próprio relatório.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from typing import List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
|
import openpyxl
|
||||||
import pdfplumber
|
import pdfplumber
|
||||||
|
|
||||||
from portal_api.planos_saude.modelos import Individuo, ItemAuditoria, Lancamento
|
from portal_api.planos_saude.modelos import Individuo, ItemAuditoria, Lancamento, normaliza_cpf, normaliza_nome
|
||||||
from portal_api.planos_saude.operadoras.base import OperadoraParser
|
from portal_api.planos_saude.operadoras.base import OperadoraParser
|
||||||
|
|
||||||
_ROW_RE = re.compile(r'^\s*(?P<numero>\d{9})\s+(?P<resto>.+)$')
|
_ROW_RE = re.compile(r'^\s*(?P<numero>\d{9})\s+(?P<resto>.+)$')
|
||||||
@ -69,10 +96,70 @@ def _valor_para_float(texto: str) -> float:
|
|||||||
return -valor if negativo else valor
|
return -valor if negativo else valor
|
||||||
|
|
||||||
|
|
||||||
|
# Nomes de coluna esperados no cabeçalho do Excel (898), já normalizados
|
||||||
|
# (maiúsculo, sem acento) via `normaliza_nome` — usado só pra ACHAR a linha
|
||||||
|
# de cabeçalho dentro do arquivo, não como chave de casamento.
|
||||||
|
_COLUNAS_XLSX_ESPERADAS = ("CODIGO", "BENEFICIARIO", "CPF", "TIPO", "MENSALIDADE")
|
||||||
|
|
||||||
|
|
||||||
class AmilOdontoMensalidade(OperadoraParser):
|
class AmilOdontoMensalidade(OperadoraParser):
|
||||||
nome_operadora = "AMIL"
|
nome_operadora = "AMIL"
|
||||||
chave_casamento = "cpf" # Amil manda CPF de todo mundo
|
chave_casamento = "cpf" # Amil manda CPF de todo mundo
|
||||||
|
|
||||||
|
def _localiza_cabecalho_xlsx(self, ws: "openpyxl.worksheet.worksheet.Worksheet") -> Tuple[int, Dict[str, int]]:
|
||||||
|
"""Acha a linha de cabeçalho de verdade dentro das primeiras linhas
|
||||||
|
do relatório (acima dela há linhas de título/contrato/fatura cuja
|
||||||
|
quantidade varia, então não é seguro fixar um número de linha) —
|
||||||
|
procura pelo NOME das colunas esperadas (`_COLUNAS_XLSX_ESPERADAS`),
|
||||||
|
não pela posição. Devolve `(número da linha, {coluna: índice})`."""
|
||||||
|
for numero_linha, linha in enumerate(ws.iter_rows(min_row=1, max_row=20, values_only=True), start=1):
|
||||||
|
indices: Dict[str, int] = {}
|
||||||
|
for indice, valor in enumerate(linha):
|
||||||
|
if valor is None:
|
||||||
|
continue
|
||||||
|
indices[normaliza_nome(str(valor))] = indice
|
||||||
|
if all(coluna in indices for coluna in _COLUNAS_XLSX_ESPERADAS):
|
||||||
|
return numero_linha, indices
|
||||||
|
raise ValueError(
|
||||||
|
"Layout do Excel da Amil Odonto não reconhecido — cabeçalho "
|
||||||
|
"esperado (Código/Beneficiário/CPF/Tipo/Mensalidade) não "
|
||||||
|
"encontrado nas primeiras linhas do arquivo."
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extrai_xlsx(self, caminho_xlsx: str) -> List[Lancamento]:
|
||||||
|
wb = openpyxl.load_workbook(caminho_xlsx, data_only=True, read_only=True)
|
||||||
|
ws = wb.worksheets[0]
|
||||||
|
linha_cabecalho, col = self._localiza_cabecalho_xlsx(ws)
|
||||||
|
|
||||||
|
lancamentos: List[Lancamento] = []
|
||||||
|
for linha in ws.iter_rows(min_row=linha_cabecalho + 1, values_only=True):
|
||||||
|
codigo = linha[col["CODIGO"]] if col["CODIGO"] < len(linha) else None
|
||||||
|
if codigo is None:
|
||||||
|
continue # linha em branco/rodapé, sem código de beneficiário
|
||||||
|
|
||||||
|
tipo = str(linha[col["TIPO"]] or "").strip() if col["TIPO"] < len(linha) else ""
|
||||||
|
if tipo not in ("T", "D", "A"):
|
||||||
|
continue # linha de rodapé/total (ex.: sem "Tipo" válido)
|
||||||
|
|
||||||
|
nome = str(linha[col["BENEFICIARIO"]] or "").strip() if col["BENEFICIARIO"] < len(linha) else ""
|
||||||
|
cpf_bruto = str(linha[col["CPF"]] or "") if col["CPF"] < len(linha) else ""
|
||||||
|
valor_bruto = linha[col["MENSALIDADE"]] if col["MENSALIDADE"] < len(linha) else 0
|
||||||
|
|
||||||
|
lancamentos.append(Lancamento(
|
||||||
|
numero_beneficiario=str(codigo),
|
||||||
|
nome=nome,
|
||||||
|
# CPF vem com máscara e como texto no arquivo real, mas
|
||||||
|
# `.zfill(11)` protege contra um export numérico futuro que
|
||||||
|
# perca o(s) zero(s) à esquerda (mesmo cuidado da MetLife,
|
||||||
|
# ver operadoras/metlife/odonto_mensalidade.py).
|
||||||
|
cpf=normaliza_cpf(cpf_bruto).zfill(11),
|
||||||
|
tipo=tipo,
|
||||||
|
rubrica="Mensalidade",
|
||||||
|
valor=float(valor_bruto or 0),
|
||||||
|
tipo_lancamento="mensalidade",
|
||||||
|
))
|
||||||
|
return lancamentos
|
||||||
|
|
||||||
def _pdf_para_linhas(self, caminho_pdf: str) -> List[str]:
|
def _pdf_para_linhas(self, caminho_pdf: str) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Extrai o texto preservando o layout de colunas (equivalente ao
|
Extrai o texto preservando o layout de colunas (equivalente ao
|
||||||
@ -136,7 +223,10 @@ class AmilOdontoMensalidade(OperadoraParser):
|
|||||||
return [individuos[c] for c in ordem]
|
return [individuos[c] for c in ordem]
|
||||||
|
|
||||||
def extrai(self, caminho_arquivo: str) -> Tuple[List[Individuo], List[ItemAuditoria]]:
|
def extrai(self, caminho_arquivo: str) -> Tuple[List[Individuo], List[ItemAuditoria]]:
|
||||||
linhas = self._pdf_para_linhas(caminho_arquivo)
|
if caminho_arquivo.lower().endswith((".xlsx", ".xlsm")):
|
||||||
lancamentos = self._parseia_lancamentos(linhas)
|
lancamentos = self._extrai_xlsx(caminho_arquivo)
|
||||||
|
else:
|
||||||
|
linhas = self._pdf_para_linhas(caminho_arquivo)
|
||||||
|
lancamentos = self._parseia_lancamentos(linhas)
|
||||||
individuos = self._agrega_por_individuo(lancamentos)
|
individuos = self._agrega_por_individuo(lancamentos)
|
||||||
return individuos, [] # Amil não gera itens de auditoria na extração
|
return individuos, [] # Amil não gera itens de auditoria na extração
|
||||||
|
|||||||
@ -85,6 +85,38 @@ def _regra_unimed_1778_tecnomyl(linhas_e_valores: List[Tuple[Any, float]], tipo_
|
|||||||
_aplica_teto_familia(linhas_e_valores, teto=661.61)
|
_aplica_teto_familia(linhas_e_valores, teto=661.61)
|
||||||
|
|
||||||
|
|
||||||
|
def _regra_amil_898_tecnomyl(linhas_e_valores: List[Tuple[Any, float]], tipo_lancamento: str) -> None:
|
||||||
|
"""Amil Odonto (código 898) na Tecnomyl, repassada pelo cliente em
|
||||||
|
09/2026: a empresa custeia 100% da mensalidade de titular e dependente
|
||||||
|
direto (cônjuge, filho(a)); agregados — avós, tios, sobrinhos, sogros e
|
||||||
|
similares — têm a mensalidade 100% descontada do empregado, no MESMO
|
||||||
|
valor (a regra é só sobre quem paga, não sobre um teto/percentual, por
|
||||||
|
isso cobre os dois planos do contrato — E200 a R$ 20,81/pessoa e
|
||||||
|
"DENTAL 200" a R$ 16,01/pessoa — sem precisar de nenhum valor fixo
|
||||||
|
aqui). Confirmado contra o arquivo real da competência 08/2026: todo
|
||||||
|
titular/dependente/agregado paga exatamente o mesmo valor por pessoa
|
||||||
|
dentro do mesmo plano, e a discriminação titular/dependente/agregado do
|
||||||
|
relatório (coluna "Tipo": T/D/A) bate exatamente com os exemplos do
|
||||||
|
cliente (avós, sogros, sobrinhos = agregado).
|
||||||
|
|
||||||
|
Ao contrário da Tecnomyl-Unimed (`_aplica_teto_familia`, um teto por
|
||||||
|
família), esta regra é por PESSOA, sem nenhuma interação entre membros
|
||||||
|
da família — usa `linha.tipo_pessoa` ('T'/'D'/'A', gravado pelo
|
||||||
|
matcher no momento do casamento a partir do Individuo.tipo original do
|
||||||
|
arquivo da operadora — ver `LinhaSistema.tipo_pessoa` — já que a
|
||||||
|
planilha padrão do sistema não distingue dependente de agregado por si
|
||||||
|
só). Uma linha sem `tipo_pessoa` preenchido (nunca passou pelo
|
||||||
|
casamento automático — ex.: incluída manualmente via "Adicionar linha")
|
||||||
|
cai no fallback "D": custeada pela empresa, o mesmo comportamento que
|
||||||
|
já valeria pra ela antes desta regra existir."""
|
||||||
|
for linha, valor in linhas_e_valores:
|
||||||
|
valor = max(0.0, valor)
|
||||||
|
tipo_pessoa = getattr(linha, "tipo_pessoa", "") or "D"
|
||||||
|
eh_agregado = tipo_pessoa == "A"
|
||||||
|
linha.valor_empresa = formata_valor_br(0.0 if eh_agregado else valor)
|
||||||
|
linha.valor = formata_valor_br(valor if eh_agregado else 0.0)
|
||||||
|
|
||||||
|
|
||||||
def _regra_sulamerica_5775_ottimizza(linhas_e_valores: List[Tuple[Any, float]], tipo_lancamento: str) -> None:
|
def _regra_sulamerica_5775_ottimizza(linhas_e_valores: List[Tuple[Any, float]], tipo_lancamento: str) -> None:
|
||||||
"""Ottimizza (código 1889) na SulAmérica (5775) — critério fixo, sem
|
"""Ottimizza (código 1889) na SulAmérica (5775) — critério fixo, sem
|
||||||
depender de teto/percentual: a mensalidade do titular é 100% custeada
|
depender de teto/percentual: a mensalidade do titular é 100% custeada
|
||||||
@ -141,6 +173,22 @@ REGRAS_EMPRESA: Dict[str, dict] = {
|
|||||||
"configurado nesta importação, sem relação com esta regra."
|
"configurado nesta importação, sem relação com esta regra."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"amil_898_tecnomyl": {
|
||||||
|
"label": "1778 - Amil (Tecnomyl)",
|
||||||
|
"codigo_empresa": "1778",
|
||||||
|
"operadora": "amil_odonto_mensalidade_898",
|
||||||
|
"chave_casamento": "cpf",
|
||||||
|
"tipos_lancamento": ("mensalidade",),
|
||||||
|
"aplica": _regra_amil_898_tecnomyl,
|
||||||
|
"observacoes": (
|
||||||
|
"A Tecnomyl arca com o valor integral da mensalidade de titular e "
|
||||||
|
"dependente direto (cônjuge, filho(a)). Agregados — avós, tios, "
|
||||||
|
"sobrinhos, sogros e similares — têm a mensalidade 100% "
|
||||||
|
"descontada do empregado, no mesmo valor por pessoa. Cobre os "
|
||||||
|
"planos do contrato (E200 e DENTAL 200), cada um no seu próprio "
|
||||||
|
"valor de mensalidade."
|
||||||
|
),
|
||||||
|
},
|
||||||
"sulamerica_5775_ottimizza": {
|
"sulamerica_5775_ottimizza": {
|
||||||
"label": "1889 - SulAmérica (5775)",
|
"label": "1889 - SulAmérica (5775)",
|
||||||
"codigo_empresa": "1889",
|
"codigo_empresa": "1889",
|
||||||
|
|||||||
@ -878,6 +878,7 @@ def _nome_base_arquivo_gerado_plano_saude(importacao: Any, todas_linhas: Iterabl
|
|||||||
PLANO_SAUDE_CAMPOS_ALTERACAO = [
|
PLANO_SAUDE_CAMPOS_ALTERACAO = [
|
||||||
"codigo_empresa", "nome_func", "cpf_func", "codigo_out_emp", "data_inicial",
|
"codigo_empresa", "nome_func", "cpf_func", "codigo_out_emp", "data_inicial",
|
||||||
"nome_dependente", "cpf_dependente", "valor_empresa", "valor", "descricao",
|
"nome_dependente", "cpf_dependente", "valor_empresa", "valor", "descricao",
|
||||||
|
"tipo_pessoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1163,6 +1164,7 @@ class ImportacaoPlanoSaudeViewSet(viewsets.ModelViewSet):
|
|||||||
valor_empresa=linha.valor_empresa,
|
valor_empresa=linha.valor_empresa,
|
||||||
valor=linha.valor,
|
valor=linha.valor,
|
||||||
descricao=linha.descricao,
|
descricao=linha.descricao,
|
||||||
|
tipo_pessoa=linha.tipo_pessoa,
|
||||||
)
|
)
|
||||||
for tipo, ordem, linha in linhas_specs
|
for tipo, ordem, linha in linhas_specs
|
||||||
])
|
])
|
||||||
@ -1512,10 +1514,15 @@ class ImportacaoPlanoSaudeAuditoriaViewSet(viewsets.GenericViewSet):
|
|||||||
# 100% desconto do empregado). Grava o valor bruto aqui primeiro
|
# 100% desconto do empregado). Grava o valor bruto aqui primeiro
|
||||||
# (valor_empresa="0" é só um placeholder) e reaplica a regra em
|
# (valor_empresa="0" é só um placeholder) e reaplica a regra em
|
||||||
# toda a família, que recupera o valor bruto de cada linha como
|
# toda a família, que recupera o valor bruto de cada linha como
|
||||||
# valor_empresa + valor.
|
# valor_empresa + valor. `tipo_pessoa` também precisa ser
|
||||||
|
# gravado aqui — esta linha nunca passou pelo casamento
|
||||||
|
# automático (senão não estaria em branco pra "Vincular
|
||||||
|
# pessoa"), então uma regra que distinga dependente de agregado
|
||||||
|
# (ex.: Amil/Tecnomyl) só sabe o tipo de `item.tipo`.
|
||||||
linha.valor_empresa = "0"
|
linha.valor_empresa = "0"
|
||||||
linha.valor = formata_valor_br(float(item.valor))
|
linha.valor = formata_valor_br(float(item.valor))
|
||||||
linha.save(update_fields=["valor_empresa", "valor"])
|
linha.tipo_pessoa = item.tipo
|
||||||
|
linha.save(update_fields=["valor_empresa", "valor", "tipo_pessoa"])
|
||||||
_recalcula_familia_regra_empresa(item.importacao, linha)
|
_recalcula_familia_regra_empresa(item.importacao, linha)
|
||||||
else:
|
else:
|
||||||
regra_por_pessoa = (item.importacao.custeio_por_tipo or {}).get(item.tipo_lancamento, {})
|
regra_por_pessoa = (item.importacao.custeio_por_tipo or {}).get(item.tipo_lancamento, {})
|
||||||
@ -1794,6 +1801,7 @@ class ImportacaoPlanoSaudeDePaulaViewSet(viewsets.ModelViewSet):
|
|||||||
valor_empresa=linha.valor_empresa,
|
valor_empresa=linha.valor_empresa,
|
||||||
valor=linha.valor,
|
valor=linha.valor,
|
||||||
descricao=linha.descricao,
|
descricao=linha.descricao,
|
||||||
|
tipo_pessoa=linha.tipo_pessoa,
|
||||||
)
|
)
|
||||||
for tipo, ordem, linha in linhas_specs
|
for tipo, ordem, linha in linhas_specs
|
||||||
])
|
])
|
||||||
@ -2061,9 +2069,12 @@ class ImportacaoPlanoSaudeDePaulaAuditoriaViewSet(viewsets.GenericViewSet):
|
|||||||
regra_empresa_dados and item.tipo_lancamento in regra_empresa_dados.get("tipos_lancamento", ("mensalidade",))
|
regra_empresa_dados and item.tipo_lancamento in regra_empresa_dados.get("tipos_lancamento", ("mensalidade",))
|
||||||
)
|
)
|
||||||
if regra_empresa_cobre_este_tipo:
|
if regra_empresa_cobre_este_tipo:
|
||||||
|
# Ver ImportacaoPlanoSaudeAuditoriaViewSet.resolver — mesmo
|
||||||
|
# motivo pra gravar `tipo_pessoa` aqui.
|
||||||
linha.valor_empresa = "0"
|
linha.valor_empresa = "0"
|
||||||
linha.valor = formata_valor_br(float(item.valor))
|
linha.valor = formata_valor_br(float(item.valor))
|
||||||
linha.save(update_fields=["valor_empresa", "valor"])
|
linha.tipo_pessoa = item.tipo
|
||||||
|
linha.save(update_fields=["valor_empresa", "valor", "tipo_pessoa"])
|
||||||
_recalcula_familia_regra_empresa_de_paula(item.importacao, linha)
|
_recalcula_familia_regra_empresa_de_paula(item.importacao, linha)
|
||||||
else:
|
else:
|
||||||
regra_por_pessoa = (item.importacao.custeio_por_tipo or {}).get(item.tipo_lancamento, {})
|
regra_por_pessoa = (item.importacao.custeio_por_tipo or {}).get(item.tipo_lancamento, {})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user