Revalidação operadora SulAmerica
This commit is contained in:
parent
4ab224d9b3
commit
b61151846d
@ -397,3 +397,9 @@ Na importação da empresa **1879 (MARV Empreendimentos)**, competência 09/2026
|
||||
2. Código de serviço "ANE" (anestesia) não reconhecido por `_ITEM_COPARTICIPACAO_RE`: R$ 208,18 faltando na MARIA LUCIENE. Acrescentado.
|
||||
|
||||
Validado contra o arquivo real: 6 beneficiários, R$ 2.102,94, batendo com o "Total do Titulo" e com cada "Total da Familia". A mensalidade da mesma competência não foi afetada (12 beneficiários, R$ 5.438,89). A importação id 97, em revisão, foi criada antes da correção e precisa ser refeita.
|
||||
|
||||
### Rodada 135 — SulAmérica Odonto (4726): PDF com fonte de símbolo dava "nenhum beneficiário encontrado"
|
||||
|
||||
Na importação da empresa **792 (Weitnauer)**, competência 09/2026, a ferramenta não achou nenhum beneficiário, embora o layout fosse idêntico ao de 08/2026. Causa: o PDF novo foi gerado com uma fonte de símbolo, e o `pdfplumber` devolve cada caractere deslocado em 0xF000 na Área de Uso Privado do Unicode ("4" como U+F034, espaço como U+F020, "é" como U+F0E9). Visualmente nada muda, mas nenhum dígito casava com `_LINHA_BENEFICIARIO_RE`.
|
||||
|
||||
Corrigido em `operadoras/sulamerica/odonto_mensalidade.py` com `_normaliza_fonte_simbolo()`, aplicada a cada linha montada por `_agrupa_linhas`: converte U+F020 a U+F0FF de volta pro Latin-1 correspondente e normaliza os espaços. Arquivo sem esse deslocamento passa inalterado. Validado contra o arquivo real: 15 beneficiários (5 titulares, 10 dependentes), R$ 437,40, batendo com o "Total" e os totalizadores impressos; acentos saem corretos ("ARLINDO JOSÉ"). Se outra operadora em PDF passar a dar "nenhum beneficiário" do nada, conferir com `repr()` se o texto veio nessa faixa U+F0xx.
|
||||
|
||||
@ -47,7 +47,7 @@ Pra adicionar uma operadora nova: criar `operadoras/<nome>/<arquivo>.py` impleme
|
||||
|
||||
**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). **O mesmo relatório pode vir com fonte de símbolo** (visto em 09/2026, empresa 792): cada caractere deslocado em 0xF000 (U+F020 a U+F0FF), invisível na tela mas sem nenhum dígito "de verdade" pro regex. `_normaliza_fonte_simbolo()` desfaz isso em cada linha antes do regex (ver rodada 135 em `CHANGELOG.md` desta pasta).
|
||||
|
||||
**SulAmérica Saúde (5775, Ottimizza)** — diferente de todos os outros parsers deste pacote: não é PDF/CSV extraído de um relatório da operadora, é uma planilha `.xlsx` ("Informações Plano de Saúde - <competência>") com colunas `Empr.`/`Cod.`/`Tipo Plano`/`CPF`/`Nome`/`Benefício Mensalidade`/`Desconto Mensalidade`/`Benefício Coparticipação`/`Desconto Coparticipação` — "Benefício" é a parte que a empresa paga, "Desconto" a parte descontada do empregado, já separadas por beneficiário nessa planilha. Casamento por CPF (`chave_casamento="cpf"`). **Decisão explícita do usuário**: este parser não trata essa divisão — só soma "Benefício Mensalidade" + "Desconto Mensalidade" num único `valor_total` de mensalidade, e "Benefício Coparticipação" + "Desconto Coparticipação" num único `valor_total` de coparticipação, por beneficiário (mesmo formato de `Individuo.valor_total` usado por toda outra operadora deste pacote — nenhum campo novo em `Individuo`/`Lancamento`). Quem decide como esse total se divide entre empresa e empregado é a "Regra especial da empresa" cadastrada como "1889 - SulAmérica (5775)" (ver "Regra empresa" logo abaixo), não este parser. `numero_beneficiario` usa o CPF normalizado, não a coluna "Cod." — validado contra o arquivo real (competência 08/2026, 76 beneficiários) um deles (LOUISE LEMOS EIGAT) aparecia em duas linhas com o mesmo valor de desconto, uma delas com "Cod." salvo como número em vez de texto no Excel (perdendo precisão nos últimos dígitos: "...118" virou "...100") — como o casamento nunca usa essa coluna, ela não afeta a correção do lançamento, mas também não serve como chave de agregação confiável; usar o CPF como chave faz as duas linhas somarem (mesma regra geral "nunca tratar cada linha isoladamente"), em vez de uma sobrescrever a outra por acaso. Validado rodando `openpyxl` de fato contra o arquivo real: 88 indivíduos extraídos (75 de mensalidade + 13 de coparticipação), com os totais batendo centavo a centavo com a soma bruta das 4 colunas da planilha.
|
||||
|
||||
|
||||
@ -85,6 +85,22 @@ def _valor_br_para_float(texto: str) -> float:
|
||||
return float(texto) if texto else 0.0
|
||||
|
||||
|
||||
def _normaliza_fonte_simbolo(texto: str) -> str:
|
||||
"""Desfaz o deslocamento de fonte de símbolo visto no arquivo real de
|
||||
09/2026 (mesma empresa 792, mesmo layout do de 08/2026): cada caractere
|
||||
veio na Área de Uso Privado do Unicode, deslocado em 0xF000 ("4" como
|
||||
U+F034, espaço como U+F020, "é" como U+F0E9). Visualmente o PDF é
|
||||
idêntico, mas sem isso nenhum dígito casa com o regex e a importação dá
|
||||
"nenhum beneficiário encontrado". Também normaliza os espaços, já que o
|
||||
U+F020 convertido fica colado a espaços reais e às bordas da linha.
|
||||
Arquivos sem esse deslocamento passam inalterados (fora os espaços)."""
|
||||
convertido = "".join(
|
||||
chr(ord(c) - 0xF000) if 0xF020 <= ord(c) <= 0xF0FF else c
|
||||
for c in texto
|
||||
)
|
||||
return " ".join(convertido.split())
|
||||
|
||||
|
||||
def _agrupa_linhas(pdf: "pdfplumber.PDF") -> List[str]:
|
||||
"""Reconstrói as linhas físicas de cada página a partir de
|
||||
`extract_words()` agrupadas por posição vertical (não pelo texto já
|
||||
@ -100,7 +116,7 @@ def _agrupa_linhas(pdf: "pdfplumber.PDF") -> List[str]:
|
||||
grupos.append([w])
|
||||
for grupo in grupos:
|
||||
grupo.sort(key=lambda w: w["x0"])
|
||||
linhas.append(" ".join(w["text"] for w in grupo))
|
||||
linhas.append(_normaliza_fonte_simbolo(" ".join(w["text"] for w in grupo)))
|
||||
return linhas
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user