feat: log de acoes/envios, ordenacao/filtro, codigo sequencial, campos opcionais

- Tabela LOG_PAINEL (nova, ja criada no banco de dev) + app/db/log.py:
  toda criacao/edicao/exclusao de cliente e contato fica registrada
  (usuario, acao, sucesso/erro, detalhe). Tela /log mostra esse
  historico.
- app/n8n_client.py: leitura opcional (N8N_API_URL/N8N_API_KEY) da
  Data Table pagamentos_processados do n8n, mostrando os envios
  automaticos de chave na mesma tela /log. Degrada de forma segura
  (lista vazia) se nao configurado ou se a chamada falhar.
- Lista de clientes ganha ordenacao configuravel (codigo/razao/
  fantasia/CNPJ) via whitelist, alem da busca que ja existia.
- CODIGO duplicado agora BLOQUEIA de verdade na criacao/edicao (sem
  bypass) — antes so avisava com opcao de confirmar mesmo assim.
- Formulario de novo cliente sugere o proximo CODIGO sequencial
  (editavel), ignorando a faixa reservada para dados de teste
  (>= 900000) para nao ser contaminado por sobra de teste no banco.
- IMPLANTACAO/MENSALIDADE/JUROS_MORA/JUROS_MULTA/DATA_CONTRATO/
  VENCIMENTO_CONTRATO deixam de ser obrigatorios — em branco, gravam
  default (0,00 / hoje) em vez de bloquear o cadastro. Só CODIGO/CNPJ/
  RAZAO/FANTASIA continuam obrigatorios.
- Limpeza: removidas 10 linhas de teste vazadas (CODIGO 999902) que
  ficaram no banco de dev por sessoes de teste interrompidas.

69/69 testes passando (16 novos).
This commit is contained in:
Gustavo
2026-08-13 14:04:24 -03:00
parent bb8fd596b1
commit 9ed0bf57c2
20 changed files with 689 additions and 170 deletions
+41 -3
View File
@@ -16,12 +16,25 @@ CAMPOS_CLIENTE = [
_COLUNAS_SELECT = ", ".join(["ID", *CAMPOS_CLIENTE])
# Whitelist de colunas aceitas para ordenar a listagem — nunca interpolar
# o valor de "ordenar_por" vindo da URL direto no SQL sem passar por aqui.
ORDENACOES_VALIDAS = {
"codigo": "CODIGO",
"razao": "RAZAO",
"fantasia": "FANTASIA",
"cnpj": "CNPJ",
}
ORDENACAO_PADRAO = "razao"
class ClienteComDependenciasError(Exception):
"""Levantada ao tentar excluir um CLIENTE que ainda tem CLIENTE_CONTATO."""
def listar_clientes(busca: str | None = None, limite: int = 100, offset: int = 0) -> list[dict]:
def listar_clientes(
busca: str | None = None, limite: int = 100, offset: int = 0, ordenar_por: str = ORDENACAO_PADRAO
) -> list[dict]:
coluna_ordenacao = ORDENACOES_VALIDAS.get(ordenar_por, ORDENACOES_VALIDAS[ORDENACAO_PADRAO])
conn = get_connection()
try:
with conn.cursor() as cur:
@@ -29,19 +42,44 @@ def listar_clientes(busca: str | None = None, limite: int = 100, offset: int = 0
sql = (
f"SELECT {_COLUNAS_SELECT} FROM CLIENTE "
"WHERE CODIGO LIKE %s OR CNPJ LIKE %s OR RAZAO LIKE %s OR FANTASIA LIKE %s "
"ORDER BY RAZAO LIMIT %s OFFSET %s"
f"ORDER BY {coluna_ordenacao} LIMIT %s OFFSET %s"
)
termo = f"%{busca}%"
cur.execute(sql, (termo, termo, termo, termo, limite, offset))
else:
cur.execute(
f"SELECT {_COLUNAS_SELECT} FROM CLIENTE ORDER BY RAZAO LIMIT %s OFFSET %s", (limite, offset)
f"SELECT {_COLUNAS_SELECT} FROM CLIENTE ORDER BY {coluna_ordenacao} LIMIT %s OFFSET %s",
(limite, offset),
)
return cur.fetchall()
finally:
conn.close()
def proximo_codigo_sugerido() -> str:
"""
Sugere o próximo CODIGO sequencial (maior CODIGO numérico + 1, com zeros
à esquerda até 6 dígitos). É só uma sugestão pré-preenchida no formulário
de novo cliente — o campo continua editável, e codigo_em_uso() ainda
confere unicidade de verdade antes de gravar.
"""
conn = get_connection()
try:
with conn.cursor() as cur:
# CODIGO >= 900000 é a faixa reservada para dados de teste deste
# painel (ver tests/test_db_clientes.py) — excluída aqui para a
# sugestão não disparar lá em cima se sobrar alguma linha de teste.
cur.execute(
"SELECT MAX(CAST(CODIGO AS UNSIGNED)) AS maior FROM CLIENTE "
"WHERE CODIGO REGEXP '^[0-9]+$' AND CAST(CODIGO AS UNSIGNED) < 900000"
)
maior = cur.fetchone()["maior"]
finally:
conn.close()
proximo = (maior or 0) + 1
return str(proximo).zfill(6)
def contar_clientes(busca: str | None = None) -> int:
conn = get_connection()
try: