feat: preenchimento automatico de dados do cliente a partir do CNPJ
Ao sair do campo CNPJ no formulario de cliente, busca automaticamente
na BrasilAPI (publica, gratuita, sem chave - dados abertos da Receita)
e preenche Razao/Fantasia/Endereco/Cidade/Bairro/UF/CEP/Telefone/
E-mail, so nos campos que estiverem vazios (nunca sobrescreve o que
ja foi digitado). So funciona para CNPJ - CPF e dado pessoal, sem
consulta publica, e a rota recusa (404) se detectar CPF.
- app/cnpj_lookup.py: cliente HTTP pra BrasilAPI, degrada em silencio
(retorna None) se nao encontrar/rede cair/formato nao reconhecido
(ex.: CNPJ alfanumerico novo, que a BrasilAPI pode nao cobrir ainda).
- Nova rota GET /clientes/consulta-cnpj/{documento}, chamada via
fetch() no formulario (JS vanilla, sem framework).
- Testado ponta a ponta contra a API real.
106/106 testes passando (10 novos).
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Consulta de CNPJ na BrasilAPI (https://brasilapi.com.br/api/cnpj/v1) para
|
||||
preencher automaticamente o formulário de cliente. Fonte pública/gratuita,
|
||||
sem autenticação, mantida pela comunidade a partir de dados abertos da
|
||||
Receita Federal — não é a API oficial da Receita (que não tem consulta
|
||||
pública direta por CNPJ).
|
||||
|
||||
Só faz sentido para CNPJ (empresa) — CPF é dado pessoal e não tem consulta
|
||||
pública, então o chamador nunca deve passar um CPF aqui (ver
|
||||
app/routers/clientes.py, que checa o tipo via normalizar_documento antes).
|
||||
|
||||
Degrada de forma segura: qualquer falha (CNPJ não encontrado, rede fora do
|
||||
ar, formato que a BrasilAPI ainda não reconhece — o novo CNPJ alfanumérico,
|
||||
vigente desde 31/07/2026, pode não estar coberto por todo provedor ainda)
|
||||
retorna None. O formulário simplesmente não preenche nada sozinho nesse caso.
|
||||
"""
|
||||
import httpx
|
||||
|
||||
_BASE_URL = "https://brasilapi.com.br/api/cnpj/v1"
|
||||
|
||||
|
||||
def consultar_cnpj(cnpj_normalizado: str) -> dict | None:
|
||||
"""
|
||||
cnpj_normalizado: 14 caracteres (12 alfanuméricos + 2 dígitos), já sem
|
||||
máscara — ver app/normalize.py. Retorna um dict só com os campos que
|
||||
existem em CLIENTE, prontos para preencher o formulário, ou None se não
|
||||
encontrou/a consulta falhou.
|
||||
"""
|
||||
if len(cnpj_normalizado) != 14:
|
||||
return None
|
||||
|
||||
try:
|
||||
resposta = httpx.get(f"{_BASE_URL}/{cnpj_normalizado}", timeout=8.0)
|
||||
if resposta.status_code != 200:
|
||||
return None
|
||||
dados = resposta.json()
|
||||
except (httpx.HTTPError, ValueError):
|
||||
return None
|
||||
|
||||
if not isinstance(dados, dict):
|
||||
return None
|
||||
|
||||
logradouro = dados.get("logradouro") or ""
|
||||
numero = dados.get("numero") or ""
|
||||
endereco = f"{logradouro}, {numero}" if numero else logradouro
|
||||
|
||||
return {
|
||||
"RAZAO": dados.get("razao_social") or "",
|
||||
"FANTASIA": dados.get("nome_fantasia") or dados.get("razao_social") or "",
|
||||
"ENDERECO": endereco[:45], # CLIENTE.ENDERECO é varchar(45)
|
||||
"CIDADE": dados.get("municipio") or "",
|
||||
"BAIRRO": dados.get("bairro") or "",
|
||||
"UF": dados.get("uf") or "",
|
||||
"CEP": (dados.get("cep") or "")[:10],
|
||||
"TELEFONE": dados.get("ddd_telefone_1") or "",
|
||||
"EMAIL": dados.get("email") or "",
|
||||
}
|
||||
+20
-1
@@ -10,9 +10,10 @@ from urllib.parse import urlencode
|
||||
|
||||
import pymysql
|
||||
from fastapi import APIRouter, Depends, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .. import cnpj_lookup
|
||||
from ..auth import exigir_login
|
||||
from ..csrf import exigir_csrf, token_do_contexto
|
||||
from ..db import chaves as db_chaves
|
||||
@@ -173,6 +174,24 @@ async def criar(request: Request, usuario: str = Depends(exigir_login)):
|
||||
return _redirect_com_mensagem(f"/clientes/{novo_id}", "Cliente criado.")
|
||||
|
||||
|
||||
@router.get("/consulta-cnpj/{documento}")
|
||||
def consulta_cnpj(documento: str, usuario: str = Depends(exigir_login)):
|
||||
"""
|
||||
Usado via JS pelo formulário de cliente (ver cliente_form.html) para
|
||||
preencher automaticamente Razão/Fantasia/Endereço/etc. ao digitar um
|
||||
CNPJ. Só consulta para CNPJ (nunca CPF — dado pessoal, sem consulta
|
||||
pública). Ver app/cnpj_lookup.py para a fonte (BrasilAPI).
|
||||
"""
|
||||
documento_normalizado, formato_valido, tipo = normalizar_documento(documento)
|
||||
if not formato_valido or tipo != "CNPJ":
|
||||
return JSONResponse({"encontrado": False}, status_code=404)
|
||||
|
||||
dados = cnpj_lookup.consultar_cnpj(documento_normalizado)
|
||||
if dados is None:
|
||||
return JSONResponse({"encontrado": False}, status_code=404)
|
||||
return {"encontrado": True, "dados": dados}
|
||||
|
||||
|
||||
@router.get("/{cliente_id}", response_class=HTMLResponse)
|
||||
def detalhe(
|
||||
request: Request,
|
||||
|
||||
@@ -34,9 +34,63 @@
|
||||
<input type="number" step="0.01" id="{{ campo }}" name="{{ campo }}" value="{{ dados[campo] if dados else '' }}" {% if campo in campos_obrigatorios %}required{% endif %}>
|
||||
{% else %}
|
||||
<input type="text" id="{{ campo }}" name="{{ campo }}" value="{{ dados[campo] if dados else '' }}" {% if campo in campos_obrigatorios %}required{% endif %}>
|
||||
{% if campo == "CNPJ" %}
|
||||
<small id="cnpj-status" style="display:block;color:#a1a1aa;margin-top:4px"></small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<p><small>* campos obrigatórios. Os demais podem ficar em branco — implantação/mensalidade/juros ficam 0,00 e as datas de contrato ficam com a data de hoje até serem ajustadas.</small></p>
|
||||
<button type="submit">Salvar</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// Ao sair do campo CNPJ, busca os dados na Receita (via BrasilAPI, ver
|
||||
// app/cnpj_lookup.py) e preenche os campos abaixo — só os que estiverem
|
||||
// vazios, nunca sobrescreve o que já foi digitado. Não faz nada para CPF
|
||||
// (a rota /clientes/consulta-cnpj só responde para CNPJ).
|
||||
var $cnpj = document.getElementById('CNPJ');
|
||||
var $status = document.getElementById('cnpj-status');
|
||||
if (!$cnpj) return;
|
||||
|
||||
var CAMPOS_PARA_PREENCHER = ['RAZAO', 'FANTASIA', 'ENDERECO', 'CIDADE', 'BAIRRO', 'UF', 'CEP', 'TELEFONE', 'EMAIL'];
|
||||
|
||||
function normalizarCnpj(valor) {
|
||||
return (valor || '').toUpperCase().replace(/[^0-9A-Z]/g, '');
|
||||
}
|
||||
|
||||
async function buscarCnpj() {
|
||||
var limpo = normalizarCnpj($cnpj.value);
|
||||
if (limpo.length !== 14) return;
|
||||
|
||||
if ($status) $status.textContent = 'Buscando dados do CNPJ...';
|
||||
|
||||
try {
|
||||
var resposta = await fetch('/clientes/consulta-cnpj/' + encodeURIComponent(limpo));
|
||||
var corpo = await resposta.json();
|
||||
if (!resposta.ok || !corpo.encontrado) {
|
||||
if ($status) $status.textContent = 'CNPJ não encontrado na Receita — preencha manualmente.';
|
||||
return;
|
||||
}
|
||||
var preencheu = false;
|
||||
CAMPOS_PARA_PREENCHER.forEach(function (campo) {
|
||||
var $input = document.getElementById(campo);
|
||||
if ($input && !$input.value && corpo.dados[campo]) {
|
||||
$input.value = corpo.dados[campo];
|
||||
preencheu = true;
|
||||
}
|
||||
});
|
||||
if ($status) {
|
||||
$status.textContent = preencheu
|
||||
? 'Dados preenchidos automaticamente a partir do CNPJ — confira antes de salvar.'
|
||||
: 'CNPJ encontrado (campos já preenchidos não foram alterados).';
|
||||
}
|
||||
} catch (erro) {
|
||||
if ($status) $status.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
$cnpj.addEventListener('blur', buscarCnpj);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user