Files
GTI_GERENCIADOR_FISCAL/app/normalize.py
T
GustavoandClaude Sonnet 5 dea9dc4401 fix: corrige achados da revisao final (DDI idempotente, mascara DDI 12/13 digitos, status DV, validacao no load, autofill dispara mascaras)
Endereca 5 achados da revisao final de branch:
- Important #1: normalizar_telefone_com_ddi agora e idempotente (nao duplica 55 independente do tamanho)
- Important #2: mascara de telefone detecta DDI colado por tamanho total (12/13 digitos), nao mais so pelo campo especifico
- Minor #3: buscarCnpj pula lookup na Receita quando DV do CNPJ e invalido, preservando a mensagem de erro
- Minor #4: detectarEValidarDocumento roda uma vez no carregamento da pagina
- Minor #5: autofill da Receita dispara evento input para acionar as mascaras de UF/CEP/TELEFONE

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 12:31:20 -03:00

120 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Funções de normalização e validação de documento (CPF/CNPJ) e telefone.
Formato de CNPJ alfanumérico vigente desde 31/07/2026 (Instrução Normativa
RFB nº 2.229/2024, Nota Técnica COCAD/SUARA/RFB nº 49/2024): 14 posições,
sendo as 12 primeiras alfanuméricas (0-9, A-Z — raiz + filial) e as 2
últimas numéricas (dígitos verificadores, módulo 11 sobre valor ASCII − 48
de cada caractere). CNPJs numéricos antigos continuam válidos e geram o
mesmo DV pela fórmula nova (retrocompatível).
`normalizar_documento` valida só o FORMATO (tamanho/charset) — é o mesmo
contrato já usado no workflow n8n (node "05 - Normalizar CNPJ") e não deve
divergir dele. `validar_dv_cpf`/`validar_dv_cnpj` são checagens ADICIONAIS
de dígito verificador, usadas pela UI como aviso (não bloqueio) — o legado
aceita documentos com DV matematicamente inválido.
"""
import re
_RE_NAO_ALFANUM = re.compile(r"[^0-9A-Z]")
_RE_CPF = re.compile(r"^[0-9]{11}$")
_RE_CNPJ = re.compile(r"^[0-9A-Z]{12}[0-9]{2}$")
_RE_NAO_DIGITO = re.compile(r"[^0-9]")
def normalizar_documento(bruto: str | None) -> tuple[str, bool, str | None]:
"""
Normaliza CPF/CNPJ: maiúsculas, só dígitos e letras A-Z (remove pontos,
barras, hífens, espaços).
CPF: 11 dígitos numéricos.
CNPJ: 12 caracteres alfanuméricos (0-9, A-Z) + 2 dígitos verificadores
numéricos.
Retorna (documento_normalizado, formato_valido, tipo) onde tipo é "CPF",
"CNPJ" ou None se inválido.
"""
limpo = _RE_NAO_ALFANUM.sub("", (bruto or "").upper())
eh_cpf = bool(_RE_CPF.match(limpo))
eh_cnpj = bool(_RE_CNPJ.match(limpo))
tipo = "CPF" if eh_cpf else ("CNPJ" if eh_cnpj else None)
return limpo, (eh_cpf or eh_cnpj), tipo
def normalizar_telefone(bruto: str | None) -> str:
"""Remove tudo que não for dígito (espaços, parênteses, traços, "+")."""
return _RE_NAO_DIGITO.sub("", bruto or "")
_RE_NAO_LETRA = re.compile(r"[^A-Z]")
def normalizar_telefone_com_ddi(bruto: str | None) -> str:
"""
Remove máscara (normalizar_telefone) e garante o DDI 55 embutido no
início — usado para TELEFONE/CELULAR de CLIENTE, que passam a ser
gravados sempre com DDI, mesmo que o usuário não digite (mesmo padrão
que CLIENTE_CONTATO.TELEFONE já usa para envio de WhatsApp).
Um valor que já começa com "55" é tratado como já tendo DDI (não
duplica), independente do tamanho — garante idempotência mesmo em
round-trips (ex.: reenvio do formulário já normalizado). Qualquer
outro valor não vazio recebe "55" na frente. Vazio continua vazio
(campo não obrigatório).
"""
digitos = normalizar_telefone(bruto)
if not digitos:
return ""
if digitos.startswith("55"):
return digitos
return "55" + digitos
def normalizar_uf(bruto: str | None) -> str:
"""Só letras, maiúsculas, no máximo 2 caracteres."""
limpo = _RE_NAO_LETRA.sub("", (bruto or "").upper())
return limpo[:2]
def normalizar_cep(bruto: str | None) -> str:
"""Só dígitos, no máximo 8 caracteres (sem traço)."""
return _RE_NAO_DIGITO.sub("", bruto or "")[:8]
_PESOS_CPF_DV1 = [10, 9, 8, 7, 6, 5, 4, 3, 2]
_PESOS_CPF_DV2 = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2]
_PESOS_CNPJ_DV1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
_PESOS_CNPJ_DV2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
def _digito_verificador(valores: list[int], pesos: list[int]) -> int:
soma = sum(v * p for v, p in zip(valores, pesos))
resto = soma % 11
return 0 if resto < 2 else 11 - resto
def validar_dv_cpf(cpf_normalizado: str) -> bool:
"""Confere os 2 dígitos verificadores de um CPF já normalizado (11 dígitos)."""
digitos = [int(c) for c in cpf_normalizado]
dv1 = _digito_verificador(digitos[:9], _PESOS_CPF_DV1)
dv2 = _digito_verificador(digitos[:9] + [dv1], _PESOS_CPF_DV2)
return digitos[9] == dv1 and digitos[10] == dv2
def _valor_caractere(c: str) -> int:
"""Valor ASCII − 48: dígitos 0-9 valem 0-9, letras A-Z valem 17-42."""
return ord(c) - 48
def validar_dv_cnpj(cnpj_normalizado: str) -> bool:
"""
Confere os 2 dígitos verificadores de um CNPJ já normalizado (12
alfanuméricos + 2 numéricos). Funciona tanto para CNPJ numérico antigo
quanto para o novo formato alfanumérico (mesma fórmula, retrocompatível).
"""
base = [_valor_caractere(c) for c in cnpj_normalizado[:12]]
dv_informado = [int(c) for c in cnpj_normalizado[12:14]]
dv1 = _digito_verificador(base, _PESOS_CNPJ_DV1)
dv2 = _digito_verificador(base + [dv1], _PESOS_CNPJ_DV2)
return dv_informado[0] == dv1 and dv_informado[1] == dv2