Files
GTI_GERENCIADOR_FISCAL/app/templates/cliente_form.html
T
GustavoandClaude Sonnet 5 f940ff5768 feat: bloqueia CNPJ/CPF duplicado ao cadastrar ou editar cliente
Mesmo padrão já usado pro CODIGO (db_clientes.codigo_em_uso): nova
db_clientes.cnpj_em_uso(documento, ignorar_id=None) checada em
criar()/editar() antes de gravar. A comparação ignora pontuação dos dois
lados (não só normaliza o valor recebido) porque CLIENTE.CNPJ tem
formatação inconsistente em registros legados (alguns com máscara,
outros sem) — sem isso, um cadastro novo com o mesmo documento de um
legado ainda mascarado passaria batido.

app/templates/cliente_form.html: nova mensagem de erro
'erro_cnpj_duplicado', mesmo estilo visual do erro de código duplicado.

6 testes novos (unitário do db + integração do router pros dois
fluxos), suíte completa (196 testes) passando.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 09:13:43 -03:00

225 lines
8.9 KiB
HTML
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.
<!-- app/templates/cliente_form.html -->
{% extends "base.html" %}
{% block titulo %}{{ "Editar" if cliente else "Novo" }} cliente{% endblock %}
{% block conteudo %}
<h1>{{ "Editar" if cliente else "Novo" }} cliente</h1>
{% if erro_codigo_duplicado %}
<div class="mensagem danger">
O código {{ dados.CODIGO }} já está em uso por outro cliente — escolha outro código antes de salvar.
</div>
{% endif %}
{% if erro_cnpj_duplicado %}
<div class="mensagem danger">
O CNPJ/CPF {{ dados.CNPJ }} já está cadastrado para outro cliente.
</div>
{% endif %}
{% if aviso_dv_invalido %}
<div class="mensagem warning">
O dígito verificador do documento ({{ dados.CNPJ }}) não confere — confirme se digitou certo antes de salvar.
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{% for campo, valor in dados.items() %}<input type="hidden" name="{{ campo }}" value="{{ valor }}">{% endfor %}
<input type="hidden" name="confirmar_dv_invalido" value="1">
<button type="submit">Cadastrar mesmo assim</button>
</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 }}
</div>
{% endif %}
<div class="cartao">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="grid-form">
{% for campo in campos %}
{% set largura_total = campo in ["OBS"] %}
<div class="{{ 'grid-form-largura-total' if largura_total }}">
<label for="{{ campo }}">{{ campo }}{% if campo in campos_obrigatorios %} *{% endif %}</label>
{% if campo in ["DATA_CONTRATO", "VENCIMENTO_CONTRATO"] %}
<input type="date" id="{{ campo }}" name="{{ campo }}" value="{{ dados[campo] if dados else '' }}" {% if campo in campos_obrigatorios %}required{% endif %}>
{% elif campo in ["IMPLANTACAO", "MENSALIDADE", "JUROS_MORA", "JUROS_MULTA"] %}
<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:var(--cor-texto-secundario);margin-top:4px"></small>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
<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" id="botao-salvar">Salvar</button>
</form>
</div>
<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 $botaoSalvar = document.getElementById('botao-salvar');
// Mesma fórmula de dígito verificador de app/normalize.py
// (_digito_verificador + validar_dv_cnpj) — módulo 11 sobre valor ASCII
// − 48 de cada caractere, suporta CNPJ numérico e o alfanumérico
// vigente desde 31/07/2026.
var PESOS_CNPJ_DV1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
var PESOS_CNPJ_DV2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
function digitoVerificador(valores, pesos) {
var soma = 0;
for (var i = 0; i < valores.length; i++) soma += valores[i] * pesos[i];
var resto = soma % 11;
return resto < 2 ? 0 : 11 - resto;
}
function valorCaractere(c) {
return c.charCodeAt(0) - 48;
}
function validarDvCnpj(cnpj14) {
var base = [];
for (var i = 0; i < 12; i++) base.push(valorCaractere(cnpj14[i]));
var dvInformado = [parseInt(cnpj14[12], 10), parseInt(cnpj14[13], 10)];
var dv1 = digitoVerificador(base, PESOS_CNPJ_DV1);
var dv2 = digitoVerificador(base.concat([dv1]), PESOS_CNPJ_DV2);
return dvInformado[0] === dv1 && dvInformado[1] === dv2;
}
function detectarEValidarDocumento() {
var limpo = normalizarCnpj($cnpj.value);
if ($botaoSalvar) $botaoSalvar.disabled = false;
if (/^[0-9]{11}$/.test(limpo)) {
if ($status) {
$status.textContent = 'CPF detectado.';
$status.style.color = 'var(--cor-texto-secundario)';
}
return;
}
if (/^[0-9A-Z]{12}[0-9]{2}$/.test(limpo)) {
if (validarDvCnpj(limpo)) {
if ($status) {
$status.textContent = 'CNPJ detectado.';
$status.style.color = 'var(--cor-texto-secundario)';
}
} else {
if ($status) {
$status.textContent = 'CNPJ inválido (dígito verificador não confere).';
$status.style.color = 'var(--cor-perigo)';
}
if ($botaoSalvar) $botaoSalvar.disabled = true;
}
return;
}
// Tamanho ainda incompleto/indeterminado: não bloqueia, não rotula.
if ($status) $status.textContent = '';
}
$cnpj.addEventListener('input', detectarEValidarDocumento);
detectarEValidarDocumento();
function aplicarMascaraTelefone(elemento, digitosAposDdd) {
elemento.addEventListener('input', function () {
var digitos = elemento.value.replace(/\D/g, '');
// DDI 55 colado: detecta por tamanho total (12 ou 13 dígitos), não
// pelo tamanho esperado do campo específico — um número de 12
// dígitos colado no campo de celular ainda deve ter o 55 removido.
if ((digitos.length === 12 || digitos.length === 13) && digitos.slice(0, 2) === '55') {
digitos = digitos.slice(2);
}
digitos = digitos.slice(0, digitosAposDdd);
var ddd = digitos.slice(0, 2);
var resto = digitos.slice(2);
var meio = resto.slice(0, digitosAposDdd - 6);
var fim = resto.slice(digitosAposDdd - 6);
var formatado = '';
if (ddd) formatado += '(' + ddd;
if (ddd.length === 2) formatado += ') ';
formatado += meio;
if (fim) formatado += '-' + fim;
elemento.value = formatado;
});
}
var $telefone = document.getElementById('TELEFONE');
var $celular = document.getElementById('CELULAR');
// TELEFONE: DDD(2) + 8 dígitos = 10 dígitos após o DDI.
if ($telefone) aplicarMascaraTelefone($telefone, 10);
// CELULAR: DDD(2) + 9 dígitos (com o 9 na frente) = 11 dígitos após o DDI.
if ($celular) aplicarMascaraTelefone($celular, 11);
var $uf = document.getElementById('UF');
if ($uf) {
$uf.addEventListener('input', function () {
$uf.value = $uf.value.toUpperCase().replace(/[^A-Z]/g, '').slice(0, 2);
});
}
var $cep = document.getElementById('CEP');
if ($cep) {
$cep.addEventListener('input', function () {
var digitos = $cep.value.replace(/\D/g, '').slice(0, 8);
$cep.value = digitos.length > 5 ? digitos.slice(0, 5) + '-' + digitos.slice(5) : digitos;
});
}
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 (limpo.length === 14 && !validarDvCnpj(limpo)) 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];
$input.dispatchEvent(new Event('input'));
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 %}