feat: bloquear CNPJ com DV inválido, gravar telefone/UF/CEP normalizados
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8b5e7e251e
commit
59f15d9ac0
+48
-18
@@ -20,7 +20,14 @@ from ..db import chaves as db_chaves
|
||||
from ..db import clientes as db_clientes
|
||||
from ..db import contatos as db_contatos
|
||||
from ..db import log as db_log
|
||||
from ..normalize import normalizar_documento, normalizar_telefone, validar_dv_cpf, validar_dv_cnpj
|
||||
from ..normalize import (
|
||||
normalizar_documento,
|
||||
normalizar_telefone_com_ddi,
|
||||
normalizar_uf,
|
||||
normalizar_cep,
|
||||
validar_dv_cpf,
|
||||
validar_dv_cnpj,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/clientes")
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
@@ -46,15 +53,17 @@ def _ler_form_cliente(form: dict) -> dict:
|
||||
|
||||
def _normalizar_documento_e_telefones(dados: dict) -> None:
|
||||
"""
|
||||
Grava CNPJ/CPF e telefones sempre sem máscara (só dígitos/letras),
|
||||
igual ao que CLIENTE_CONTATO.TELEFONE já faz — evita que CLIENTE fique
|
||||
com dois formatos diferentes convivendo. Só afeta escritas novas feitas
|
||||
por este painel; registros legados só passam a ficar sem máscara quando
|
||||
forem reeditados e salvos aqui.
|
||||
Grava CNPJ/CPF sempre sem máscara, TELEFONE/CELULAR sempre com DDI 55
|
||||
embutido, UF em maiúsculas (2 letras) e CEP só com dígitos — evita que
|
||||
CLIENTE fique com formatos inconsistentes convivendo. Só afeta
|
||||
escritas novas feitas por este painel; registros legados só passam a
|
||||
ficar normalizados quando forem reeditados e salvos aqui.
|
||||
"""
|
||||
dados["CNPJ"] = normalizar_documento(dados["CNPJ"])[0]
|
||||
dados["TELEFONE"] = normalizar_telefone(dados["TELEFONE"])
|
||||
dados["CELULAR"] = normalizar_telefone(dados["CELULAR"])
|
||||
dados["TELEFONE"] = normalizar_telefone_com_ddi(dados["TELEFONE"])
|
||||
dados["CELULAR"] = normalizar_telefone_com_ddi(dados["CELULAR"])
|
||||
dados["UF"] = normalizar_uf(dados["UF"])
|
||||
dados["CEP"] = normalizar_cep(dados["CEP"])
|
||||
|
||||
|
||||
def _aplicar_defaults_opcionais(dados: dict) -> None:
|
||||
@@ -77,6 +86,29 @@ def _campos_obrigatorios_faltando(dados: dict) -> list[str]:
|
||||
return [campo for campo in CAMPOS_OBRIGATORIOS if not dados.get(campo)]
|
||||
|
||||
|
||||
def _validar_documento(dados: dict, confirmar_dv: bool) -> dict:
|
||||
"""
|
||||
Confere o dígito verificador do documento em CLIENTE.CNPJ (o mesmo
|
||||
campo aceita CPF ou CNPJ, ver app/normalize.py). CNPJ com DV inválido
|
||||
bloqueia sempre — não existe "cadastrar mesmo assim" para CNPJ. CPF
|
||||
com DV inválido mantém o fluxo já existente: aviso + confirmação.
|
||||
|
||||
Retorna um dict para mesclar no contexto do template: {} se o
|
||||
documento está ok (ou é indeterminado), ou uma das chaves de erro que
|
||||
cliente_form.html já sabe renderizar.
|
||||
"""
|
||||
documento_normalizado, formato_valido, tipo = normalizar_documento(dados["CNPJ"])
|
||||
if not formato_valido:
|
||||
return {}
|
||||
if tipo == "CNPJ":
|
||||
if not validar_dv_cnpj(documento_normalizado):
|
||||
return {"erro_cnpj_invalido": True}
|
||||
return {}
|
||||
if not validar_dv_cpf(documento_normalizado) and not confirmar_dv:
|
||||
return {"aviso_dv_invalido": True}
|
||||
return {}
|
||||
|
||||
|
||||
@router.get("", response_class=HTMLResponse)
|
||||
def listar(
|
||||
request: Request,
|
||||
@@ -151,16 +183,9 @@ async def criar(request: Request, usuario: str = Depends(exigir_login)):
|
||||
request, "cliente_form.html", {**contexto_base, "erro_codigo_duplicado": True}
|
||||
)
|
||||
|
||||
documento_normalizado, formato_valido, tipo = normalizar_documento(dados["CNPJ"])
|
||||
dv_invalido = False
|
||||
if formato_valido:
|
||||
dv_invalido = not (
|
||||
validar_dv_cpf(documento_normalizado) if tipo == "CPF" else validar_dv_cnpj(documento_normalizado)
|
||||
)
|
||||
if dv_invalido and not confirmar_dv:
|
||||
return templates.TemplateResponse(
|
||||
request, "cliente_form.html", {**contexto_base, "aviso_dv_invalido": True}
|
||||
)
|
||||
extra_erro = _validar_documento(dados, confirmar_dv)
|
||||
if extra_erro:
|
||||
return templates.TemplateResponse(request, "cliente_form.html", {**contexto_base, **extra_erro})
|
||||
|
||||
_aplicar_defaults_opcionais(dados)
|
||||
try:
|
||||
@@ -237,6 +262,7 @@ async def editar(request: Request, cliente_id: int, usuario: str = Depends(exigi
|
||||
form = dict((await request.form()))
|
||||
dados = _ler_form_cliente(form)
|
||||
_normalizar_documento_e_telefones(dados)
|
||||
confirmar_dv = form.get("confirmar_dv_invalido") == "1"
|
||||
|
||||
contexto_base = {
|
||||
"cliente": {"ID": cliente_id}, "dados": dados, "campos": db_clientes.CAMPOS_CLIENTE,
|
||||
@@ -255,6 +281,10 @@ async def editar(request: Request, cliente_id: int, usuario: str = Depends(exigi
|
||||
request, "cliente_form.html", {**contexto_base, "erro_codigo_duplicado": True}
|
||||
)
|
||||
|
||||
extra_erro = _validar_documento(dados, confirmar_dv)
|
||||
if extra_erro:
|
||||
return templates.TemplateResponse(request, "cliente_form.html", {**contexto_base, **extra_erro})
|
||||
|
||||
_aplicar_defaults_opcionais(dados)
|
||||
try:
|
||||
db_clientes.atualizar_cliente(cliente_id, dados)
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if erro_cnpj_invalido %}
|
||||
<div class="mensagem danger">
|
||||
O dígito verificador do CNPJ ({{ dados.CNPJ }}) não confere — corrija antes de salvar.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if erro_gravacao %}
|
||||
<div class="mensagem danger">
|
||||
Não foi possível salvar: {{ erro_gravacao }}
|
||||
|
||||
Reference in New Issue
Block a user