Inclusão do Plano de Saude - MetLife
This commit is contained in:
parent
429a64d4a5
commit
4c622f7013
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: importacao-plano-saude
|
||||
description: Guia de manutenção/extensão da etapa de leitura e validação da ferramenta "Importação de Plano de Saúde" do Portal De Paula (portal_api/planos_saude/, tela importacao-plano-saude.html) — extração dos relatórios de operadora (11 parsers já implementados), casamento contra a planilha padrão e regras de negócio (regra de custeio, "Regra empresa", vínculos de nome), independente do sistema contábil de destino. Documenta quais operadoras já estão validadas com dado real, os gaps conhecidos por operadora, e como adicionar uma operadora nova. Direciona pra uma skill específica de sistema contábil (Questor hoje; Contabit no futuro) pra tudo que for leiaute/geração de arquivo final. Usar ao dar manutenção nos parsers, ao investigar uma divergência de valores, ou ao decidir se uma operadora nova pode ser cadastrada com segurança.
|
||||
description: Guia de manutenção/extensão da etapa de leitura e validação da ferramenta "Importação de Plano de Saúde" do Portal De Paula (portal_api/planos_saude/, tela importacao-plano-saude.html) — extração dos relatórios de operadora (12 parsers já implementados), casamento contra a planilha padrão e regras de negócio (regra de custeio, "Regra empresa", vínculos de nome), independente do sistema contábil de destino. Documenta quais operadoras já estão validadas com dado real, os gaps conhecidos por operadora, e como adicionar uma operadora nova. Direciona pra uma skill específica de sistema contábil (Questor hoje; Contabit no futuro) pra tudo que for leiaute/geração de arquivo final. Usar ao dar manutenção nos parsers, ao investigar uma divergência de valores, ou ao decidir se uma operadora nova pode ser cadastrada com segurança.
|
||||
---
|
||||
|
||||
# Importação de Plano de Saúde: leitura, extração e regras de negócio
|
||||
@ -15,7 +15,7 @@ Este SKILL.md documenta a etapa **comum a qualquer sistema contábil de destino*
|
||||
|
||||
A ferramenta atende hoje várias empresas/operadoras reais, nenhuma com tratamento especial no código de extração — toda operadora passa pelo mesmo `OperadoraParser`/`pipeline.processa_importacao` genérico. **É uma fotografia, não um fato permanente**: cresce todo mês, reconsultar `ImportacaoPlanoSaude` (`python manage.py shell`) antes de confiar nela pra uma decisão importante.
|
||||
|
||||
### 1.1 Os 11 parsers de operadora existentes (12 cadastros — Amil Odonto tem dois): uso real até agora
|
||||
### 1.1 Os 12 parsers de operadora existentes (13 cadastros — Amil Odonto tem dois): uso real até agora
|
||||
|
||||
| Operadora (`pipeline.OPERADORAS`) | Casamento | Importações no banco | Alguma concluída? |
|
||||
|---|---|---|---|
|
||||
@ -31,6 +31,7 @@ A ferramenta atende hoje várias empresas/operadoras reais, nenhuma com tratamen
|
||||
| `amil_odonto_mensalidade_898` (898) | CPF | 0 até 28/08/2026 | Nunca foi rodada — segundo cadastro da mesma operadora/mesmo parser no Questor (empresas diferentes usam um código ou outro), adicionado a pedido do usuário; regras e parâmetros de extração são idênticos aos de `amil_odonto_mensalidade` (3758) |
|
||||
| `humana_saude` (5064) | nome | 0 até 27/08/2026 | Nunca foi rodada dentro da ferramenta até então — parser novo, construído via o checklist da seção 4.1 direto no primeiro teste com o arquivo-modelo (empresa 1972). **Coparticipação nunca casa automaticamente** (decisão do usuário): sem CPF nem matrícula confiável na tabela "DESPESAS COBRADAS" (nome truncado por largura de coluna), cada evento vira direto um item de auditoria — ver `operadoras/humana/saude.py` |
|
||||
| `unimed_cascavel_saude` (158) | nome | 0 até 27/08/2026 | Nunca foi rodada dentro da ferramenta até então — parser novo, mesma empresa-modelo da Humana (1972). Outra Unimed regional, layout de PDF sem nenhuma sobreposição com `unimed_saude` (5060). **Coparticipação pode vir de duas fontes possíveis pro mesmo mês** (tabela embutida no relatório de mensalidade OU extrato separado — "o modelo de arquivo é gerado pela operadora"), nunca somadas: `OperadoraParser.finaliza()` (hook novo, chamado só depois de ver todos os arquivos da importação) resolve qual usar, preferindo o extrato separado — ver `operadoras/unimed_cascavel/saude.py` |
|
||||
| `metlife_odonto_mensalidade` (3761) | CPF | 0 até 02/09/2026 | Nunca foi rodada dentro da ferramenta até então — parser novo, construído via o checklist da seção 4.1. Só mensalidade, regra de custeio padrão (sem "Regra empresa"), confirmado com o usuário. Extração por posição de coluna (`extract_words()` bucketizado por x0), não regex de linha — nome sem largura fixa. **CPF sai sem zero(s) à esquerda quando o valor real começa com 0** (corrigido com `.zfill(11)` na extração — conferir se aparecer de novo numa operadora futura com CPF aparentemente "curto"). Validado rodando `extrai()` contra o arquivo real: 8 beneficiários, R$ 120,00, batendo com o total impresso — ainda não rodado contra uma planilha padrão real (nenhuma empresa cadastrada nesta operadora até agora) — ver `operadoras/metlife/odonto_mensalidade.py` |
|
||||
|
||||
**Ressalva sobre a Bradesco Dental (3759):** o `CLAUDE.md` registra que este parser foi escrito só a partir de texto colado numa conversa, nunca confirmado contra o arquivo real. O banco, porém, já tem uma importação **concluída** pra esse operador (empresa 1684), ou seja, alguém rodou um arquivo real depois daquela ressalva ser escrita. "Concluída" só significa que o pipeline processou sem erro e o CSV foi gerado, **não** que os valores foram de fato conferidos linha a linha contra a fatura. Antes de remover a ressalva do `CLAUDE.md`, confirmar com o usuário se essa conferência manual aconteceu.
|
||||
|
||||
|
||||
123471
media/nao_conformidades/acoes/acoes_31082026_-_COM_FINALIZADAS.xls
Normal file
123471
media/nao_conformidades/acoes/acoes_31082026_-_COM_FINALIZADAS.xls
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -25,7 +25,8 @@ portal_api/planos_saude/
|
||||
├── 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)
|
||||
├── humana/saude.py Humana Saúde — 5064 (PDF via pdfplumber, mensalidade+coparticipação no mesmo arquivo, casamento por nome — coparticipação nunca casada automaticamente, ver nota abaixo)
|
||||
└── unimed_cascavel/saude.py Unimed Cascavel — 158 (PDF via pdfplumber, mensalidade+coparticipação, casamento por nome — coparticipação pode vir de duas fontes possíveis, nunca somadas juntas, ver nota abaixo e `OperadoraParser.finaliza()`)
|
||||
├── unimed_cascavel/saude.py Unimed Cascavel — 158 (PDF via pdfplumber, mensalidade+coparticipação, casamento por nome — coparticipação pode vir de duas fontes possíveis, nunca somadas juntas, ver nota abaixo e `OperadoraParser.finaliza()`)
|
||||
└── metlife/odonto_mensalidade.py MetLife Odonto — 3761 (PDF via pdfplumber, só mensalidade, casamento por CPF, extração por posição de coluna em vez de regex de linha, ver nota abaixo)
|
||||
```
|
||||
|
||||
Pra adicionar uma operadora nova: criar `operadoras/<nome>/<arquivo>.py` implementando `OperadoraParser.extrai()` (devolve `(List[Individuo], List[ItemAuditoria])`) e registrar em `pipeline.OPERADORAS`. **Antes de escrever o parser, ler `projects/importacao-planos-saude.skill`** — documenta decisões de negócio já validadas com o cliente (ex.: nome divergente nunca é resolvido por aproximação, valor final negativo vai pra auditoria, um mesmo beneficiário pode aparecer em várias linhas/rubricas e precisa ser somado) que não devem ser reinterpretadas sem confirmar de novo.
|
||||
@ -55,6 +56,13 @@ Pra adicionar uma operadora nova: criar `operadoras/<nome>/<arquivo>.py` impleme
|
||||
- **Toda resolução de família (titular/dependente) é feita por matrícula, nunca por nome** — a coluna "Usuário" do relatório de mensalidade é estreita e trunca nomes longos sem reticências (mesmo padrão de Amil/Bradesco/Humana, confirmado inspecionando os limites reais de x0/x1 do PDF: o nome pára exatamente na borda da coluna seguinte), então comparar o nome truncado da mensalidade com o nome completo do extrato de coparticipação para resolver `numero_titular`/`tipo` não seria confiável. Em vez disso, `_pessoa_por_matricula` (matrícula -> nome/tipo/numero_titular) é populado só ao processar a tabela de **mensalidade** (onde a família já vem corretamente resolvida por ordem de bloco: titular sempre antes dos próprios dependentes) e reaproveitado em `finaliza()` pra resolver os dois candidatos de coparticipação — que só carregam matrícula + valor, nada de nome. Um beneficiário com coparticipação mas ausente de toda tabela de mensalidade desta importação (arquivo daquele contrato não anexado) vira um `ItemAuditoria` explícito (`NAO_CADASTRADO`), nunca é descartado silenciosamente. Nomes truncados na mensalidade em si seguem o fluxo normal (`NOME_DIVERGENTE` em auditoria, resolvido manualmente uma vez via "Vincular pessoa" — nunca por aproximação).
|
||||
- Validado rodando `extrai()`/`finaliza()` de ponta a ponta contra os 3 arquivos reais da empresa 1972 (Fronteira Outdoor Ltda, competência 08/2026, 2 contratos — 183237 e 183210/"Estadual"): mensalidade batendo exatamente com os totais impressos (R$ 4.076,41 + R$ 808,98 = R$ 4.885,39, 12 beneficiários) e coparticipação batendo com R$ 1.067,17 (3 beneficiários), confirmando que a tabela embutida (também extraída, mesmos valores) foi corretamente descartada em favor do extrato separado, sem duplicar nada.
|
||||
|
||||
**MetLife Odonto (3761)** — relatório "Detalhamento de Mensalidade" (NF-e da própria MetLife), só mensalidade, casamento por CPF. Confirmado com o usuário: segue a regra padrão de custeio (mensalidade separada por titular/dependente), sem "Regra empresa". Diferente dos demais parsers deste pacote, a extração é feita **bucketizando `extract_words()` por posição de coluna** (x0), não por regex de linha inteira — necessário porque o Nome não tem largura fixa nem separador (ex.: "MICHELE REGINA DA SILVA EUGENIO"), então um regex de `.+?` até a próxima coluna arriscaria errar a fronteira; a mesma técnica generaliza de graça pra colunas opcionais (Parentesco/Nº Funcional, vazias em boa parte das linhas) sem precisar de grupos regex opcionais complicados.
|
||||
|
||||
- **Titular/dependente é decidido pela coluna Parentesco estar vazia ou não** (nunca pelo sufixo do código de beneficiário, ex. `.00`/`.01`) — decisão tomada depois de um caso real já no próprio arquivo-modelo: um dependente (mãe de um titular) veio com o código malformado **no próprio PDF de origem** (`50696900.000100.01`, 8 dígitos na primeira parte em vez dos 6 esperados — confirmado inspecionando `page.extract_words()` que a string malformada já existe no PDF, não é artefato de extração; a "Adesão" desse dependente era bem mais recente que o resto da família, sugerindo inclusão tardia com erro do sistema da própria operadora). A coluna Parentesco continuou confiável nesse caso, então a classificação não foi afetada; `numero_beneficiario` guarda o código bruto tal como veio (mesmo malformado), só como identificador de exibição, nunca como chave de casamento.
|
||||
- **CPF sai sem zero(s) à esquerda quando o valor real da pessoa começa com 0** — confirmado comparando com o formato real de `CPFFUNC`/`CPFDEPENDENTE` na planilha padrão do Questor (sempre 11 dígitos, `XXX.XXX.XXX-XX`): os CPFs deste relatório MetLife saem com 9 a 11 dígitos (ex.: `"795136978"`, 9 dígitos), típico de um campo numérico que perdeu o(s) zero(s) à esquerda ao ser gerado a partir de planilha. Sem corrigir, o casamento por CPF falharia silenciosamente ("CPF não encontrado") pra toda pessoa cujo CPF real começa com zero — corrigido com `.zfill(11)` na extração (`operadoras/metlife/odonto_mensalidade.py`), seguro porque CPF brasileiro sempre tem 11 dígitos.
|
||||
- Validado rodando `extrai()` de ponta a ponta contra o arquivo real (competência 09/2026, empresa "OESTEFOZ NEW CORRETORA DE SEGUROS LTDA", 3 famílias): 8 beneficiários (3 titulares + 5 dependentes), R$ 120,00 no total — bate exatamente com "TOTAL DE MENSALIDADES: 8 itens 120,00" impresso no próprio relatório. Ainda não rodado contra a planilha padrão real de nenhuma empresa (nenhuma `RegraCusteioPlanoSaude` cadastrada pra esta operadora até o momento).
|
||||
- **Não confirmado ainda**: o arquivo-modelo só tinha nomes/planos curtos, cabendo numa única linha física — nome ou plano que ultrapasse a largura da coluna e quebre em duas linhas físicas ainda não foi visto; reconferir se aparecer numa competência real.
|
||||
|
||||
**Diferença deliberada em relação ao pipeline original**: lá, o valor do mês sempre gravava na coluna `VALOR` (desconto do empregado), nunca em `VALOREMPRESA` — regra fixa. Aqui, o usuário escolhe na tela de nova importação, **por tipo de lançamento (mensalidade/coparticipação) e por tipo de beneficiário (titular/dependente)** — quatro combinações independentes, ex.: mensalidade do titular custeada pela empresa e mensalidade do dependente descontada do empregado —, uma de três regras de custeio: "Custeado pela empresa" (`{"modo": "empresa"}`), "Descontado do empregado" (`{"modo": "empregado"}`, o comportamento antigo — nomenclatura "empregado", não "funcionário", pra não confundir com `NOMEFUNC`/`CPFFUNC` do leiaute do Questor, que é outra coisa) ou "Regra específica" (`{"modo": "especifica", "limite_valor": float|None, "percentual": float|None, "limite_desconto_empregado": float|None}`).
|
||||
|
||||
Dentro da regra específica, dois grupos de critério, **mutuamente exclusivos** entre si (validado em `_monta_regra_custeio`, `serializers.py`, e refletido no formulário desabilitando um grupo assim que o outro é preenchido — `atualizarExclusividadeRegraEspecifica()`, `importacao-plano-saude.js`):
|
||||
|
||||
175
portal_api/planos_saude/operadoras/metlife/odonto_mensalidade.py
Normal file
175
portal_api/planos_saude/operadoras/metlife/odonto_mensalidade.py
Normal file
@ -0,0 +1,175 @@
|
||||
"""
|
||||
MetLife Planos Odontológicos (código de operadora 3761) - Odonto, só
|
||||
mensalidade. Casamento por CPF (confirmado com o usuário: regra padrão de
|
||||
custeio, sem "Regra empresa").
|
||||
|
||||
Relatório "Detalhamento de Mensalidade" (NF-e da própria MetLife, uma linha
|
||||
por beneficiário dentro de cada grupo familiar) — sem coparticipação
|
||||
(nenhuma tabela de serviço/atendimento no arquivo, só a cobrança fixa
|
||||
periódica por pessoa).
|
||||
|
||||
Estrutura confirmada via `page.extract_words()` (posição x0/top) contra o
|
||||
arquivo real (competência 09/2026, empresa "OESTEFOZ NEW CORRETORA DE
|
||||
SEGUROS LTDA", 3 famílias, 8 beneficiários, R$ 120,00):
|
||||
|
||||
1. Colunas do relatório (x0 aproximado, usado pra bucketizar cada palavra
|
||||
na coluna certa em vez de tentar um regex de linha inteira): Código
|
||||
(~31), Nº Funcional (~113, **sempre vazio no arquivo-modelo** — nenhum
|
||||
beneficiário testado até hoje tinha algo nessa coluna; se aparecer
|
||||
preenchida numa empresa futura, o bucket `funcional` já existe e
|
||||
simplesmente não é usado, então não quebra nada, só fica sem efeito),
|
||||
Nome (~169), Data de nasc. (~329), Parentesco (~381, **só preenchido
|
||||
pro dependente** — é o campo usado pra decidir titular/dependente, não
|
||||
o sufixo do código, ver item 3), CPF (~485), Adesão (~545), Plano
|
||||
(~597), Valor (~750+). Abordagem por posição (`_coluna_para_x0`), não
|
||||
regex de linha inteira, porque o Nome pode ter qualquer número de
|
||||
palavras (ex.: "MICHELE REGINA DA SILVA EUGENIO") sem nenhum separador
|
||||
que distinga onde ele termina — só a posição x0 resolve isso de forma
|
||||
confiável, mesma técnica de `operadoras/sulamerica/odonto_mensalidade.py`
|
||||
e `operadoras/unimed_vitoria/saude.py`.
|
||||
2. Ao final de cada família, uma linha "Total do grupo familiar: R$X" (sem
|
||||
nenhum código de beneficiário) — como as outras operadoras deste
|
||||
pacote, **nunca usada**: o lançamento é sempre por linha de
|
||||
beneficiário, nunca pelo subtotal impresso.
|
||||
3. **Titular vs. dependente é decidido pela coluna Parentesco estar vazia
|
||||
ou não** (não pelo sufixo do código, ex. ".00"/".01") — decisão
|
||||
deliberada depois de um caso real no próprio arquivo-modelo: uma
|
||||
dependente ("SONIA REGINA ROCHA", MÃE) veio com um código malformado no
|
||||
PRÓPRIO PDF de origem (`50696900.000100.01`, 8 dígitos na primeira parte
|
||||
em vez dos 6 esperados — confirmado com `page.extract_words()` que essa
|
||||
é a string real no PDF, não um artefato de extração; provável erro de
|
||||
sistema da operadora ao incluir esse dependente depois, "Adesão"
|
||||
01/08/2026, bem mais recente que o resto da família). Se a classificação
|
||||
dependesse do sufixo do código, essa pessoa teria sido malinterpretada;
|
||||
usando a coluna Parentesco (sempre confiável nos 8 casos do
|
||||
arquivo-modelo), o problema não afeta o resultado. `numero_beneficiario`
|
||||
guarda o código bruto tal como veio (mesmo malformado) — só usado como
|
||||
identificador de exibição/agregação, nunca como chave de casamento.
|
||||
4. **CPF vem sem zero à esquerda quando o valor real da pessoa começa com
|
||||
0** — confirmado comparando com a planilha padrão real do Questor
|
||||
(`CPFFUNC`/`CPFDEPENDENTE` sempre no formato `XXX.XXX.XXX-XX`, 11
|
||||
dígitos): os CPFs deste relatório MetLife saem com 9 a 11 dígitos
|
||||
(ex.: `"795136978"`, 9 dígitos — típico de um campo numérico que perdeu
|
||||
o(s) zero(s) à esquerda ao ser exportado, comum em relatórios gerados a
|
||||
partir de planilha). Sem corrigir isso, o casamento por CPF falharia
|
||||
silenciamente (viraria "CPF não encontrado") pra toda pessoa cujo CPF
|
||||
real começa com zero. Corrigido com `.zfill(11)` na extração — CPF
|
||||
brasileiro sempre tem 11 dígitos, então completar com zeros à esquerda
|
||||
é seguro e não ambíguo.
|
||||
|
||||
Validado rodando `extrai()` de ponta a ponta contra o arquivo real: 8
|
||||
beneficiários (3 titulares + 5 dependentes), R$ 120,00 no total — bate
|
||||
exatamente com "TOTAL DE MENSALIDADES: 8 itens 120,00" impresso no próprio
|
||||
relatório.
|
||||
|
||||
**Não confirmado ainda** (arquivo-modelo só tinha nomes/planos curtos,
|
||||
cabendo numa única linha física): nome ou plano que ultrapasse a largura da
|
||||
própria coluna e quebre em duas linhas físicas — reconferir se aparecer
|
||||
numa competência real com um nome bem mais longo.
|
||||
"""
|
||||
import re
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import pdfplumber
|
||||
|
||||
from portal_api.planos_saude.modelos import Individuo, ItemAuditoria, Lancamento
|
||||
from portal_api.planos_saude.operadoras.base import OperadoraParser
|
||||
|
||||
# (nome da coluna, x0 mínimo) — ordenado por x0 crescente; uma palavra cai
|
||||
# na última coluna cujo x0 mínimo ela ainda satisfaz.
|
||||
_COLUNAS = [
|
||||
("codigo", 0),
|
||||
("funcional", 113),
|
||||
("nome", 169),
|
||||
("nasc", 329),
|
||||
("parentesco", 381),
|
||||
("cpf", 485),
|
||||
("adesao", 545),
|
||||
("plano", 597),
|
||||
("valor", 750),
|
||||
]
|
||||
|
||||
_CODIGO_BENEFICIARIO_RE = re.compile(r"^\d+\.\d+\.\d+$")
|
||||
|
||||
|
||||
def _coluna_para_x0(x0: float) -> str:
|
||||
coluna = _COLUNAS[0][0]
|
||||
for nome, minimo in _COLUNAS:
|
||||
if x0 >= minimo:
|
||||
coluna = nome
|
||||
else:
|
||||
break
|
||||
return coluna
|
||||
|
||||
|
||||
def _valor_br_para_float(texto: str) -> float:
|
||||
"""'15,00' -> 15.0 '1.234,56' -> 1234.56 (formato BR)."""
|
||||
texto = texto.strip().replace(".", "").replace(",", ".")
|
||||
return float(texto) if texto else 0.0
|
||||
|
||||
|
||||
def _agrupa_linhas_por_coluna(pdf: "pdfplumber.PDF") -> List[Dict[str, str]]:
|
||||
"""Reconstrói cada linha física do relatório como um dict coluna->texto,
|
||||
bucketizando `extract_words()` por posição vertical (linha) e depois
|
||||
horizontal (coluna) — ver docstring do módulo pro motivo de não usar
|
||||
regex de linha inteira."""
|
||||
linhas: List[Dict[str, str]] = []
|
||||
for page in pdf.pages:
|
||||
palavras = sorted(page.extract_words(), key=lambda w: (w["top"], w["x0"]))
|
||||
grupos: Dict[float, list] = {}
|
||||
for w in palavras:
|
||||
grupos.setdefault(round(w["top"], 1), []).append(w)
|
||||
for top in sorted(grupos):
|
||||
buckets: Dict[str, List[str]] = {}
|
||||
for w in grupos[top]:
|
||||
buckets.setdefault(_coluna_para_x0(w["x0"]), []).append(w["text"])
|
||||
linhas.append({coluna: " ".join(textos) for coluna, textos in buckets.items()})
|
||||
return linhas
|
||||
|
||||
|
||||
class MetlifeOdontoMensalidade(OperadoraParser):
|
||||
nome_operadora = "MetLife Odonto"
|
||||
chave_casamento = "cpf" # CPF sempre presente no relatório (ver nota do zfill acima)
|
||||
|
||||
def extrai(self, caminho_arquivo: str) -> Tuple[List[Individuo], List[ItemAuditoria]]:
|
||||
with pdfplumber.open(caminho_arquivo) as pdf:
|
||||
linhas = _agrupa_linhas_por_coluna(pdf)
|
||||
|
||||
lancamentos: List[Lancamento] = []
|
||||
for linha in linhas:
|
||||
codigo = linha.get("codigo", "").strip()
|
||||
if not _CODIGO_BENEFICIARIO_RE.match(codigo):
|
||||
continue # cabeçalho, "Total do grupo familiar:", rodapé etc.
|
||||
|
||||
parentesco = linha.get("parentesco", "").strip()
|
||||
cpf = "".join(ch for ch in linha.get("cpf", "") if ch.isdigit()).zfill(11)
|
||||
lancamentos.append(Lancamento(
|
||||
numero_beneficiario=codigo,
|
||||
nome=" ".join(linha.get("nome", "").split()),
|
||||
cpf=cpf,
|
||||
tipo="D" if parentesco else "T",
|
||||
rubrica=f"Mensalidade ({linha.get('plano', '').strip()})",
|
||||
valor=_valor_br_para_float(linha.get("valor", "")),
|
||||
tipo_lancamento="mensalidade",
|
||||
))
|
||||
|
||||
individuos = self._agrega_por_individuo(lancamentos)
|
||||
return individuos, []
|
||||
|
||||
def _agrega_por_individuo(self, lancamentos: List[Lancamento]) -> List[Individuo]:
|
||||
individuos: Dict[str, Individuo] = {}
|
||||
ordem = []
|
||||
for lc in lancamentos:
|
||||
chave = lc.numero_beneficiario
|
||||
if chave not in individuos:
|
||||
individuos[chave] = Individuo(
|
||||
numero_beneficiario=lc.numero_beneficiario,
|
||||
nome=lc.nome,
|
||||
cpf=lc.cpf,
|
||||
tipo=lc.tipo,
|
||||
tipo_lancamento=lc.tipo_lancamento,
|
||||
)
|
||||
ordem.append(chave)
|
||||
individuos[chave].valor_total += lc.valor
|
||||
individuos[chave].rubricas.append(f"{lc.rubrica}: {lc.valor:+.2f}")
|
||||
return [individuos[c] for c in ordem]
|
||||
@ -20,6 +20,7 @@ from portal_api.planos_saude.operadoras.bradesco.saude import BradescoSaude
|
||||
from portal_api.planos_saude.operadoras.dental_uni.odonto_mensalidade import DentalUniOdontoMensalidade
|
||||
from portal_api.planos_saude.operadoras.humana.saude import HumanaSaude
|
||||
from portal_api.planos_saude.operadoras.itamed.saude import ItamedSaude
|
||||
from portal_api.planos_saude.operadoras.metlife.odonto_mensalidade import MetlifeOdontoMensalidade
|
||||
from portal_api.planos_saude.operadoras.sulamerica.odonto_mensalidade import SulAmericaOdontoMensalidade
|
||||
from portal_api.planos_saude.operadoras.sulamerica.saude import SulAmericaSaude
|
||||
from portal_api.planos_saude.operadoras.unimed.saude import UnimedSaude
|
||||
@ -97,6 +98,11 @@ OPERADORAS = {
|
||||
"nome": "Unimed Cascavel",
|
||||
"parser": UnimedCascavelSaude,
|
||||
},
|
||||
"metlife_odonto_mensalidade": {
|
||||
"codigo_operadora": "3761",
|
||||
"nome": "MetLife Odonto",
|
||||
"parser": MetlifeOdontoMensalidade,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
projects/project/operadoras/Metlife/DEMONSTRATIVO METLIFE.pdf
Normal file
BIN
projects/project/operadoras/Metlife/DEMONSTRATIVO METLIFE.pdf
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user