Add 'gerenciador/' from commit '34cc9c75a57dedbf61a6e6a1ba1f35f76e72434b'
git-subtree-dir: gerenciador git-subtree-mainline:f4f546c928git-subtree-split:34cc9c75a5
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
<!-- 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 %}>
|
||||
{% elif campo == "ATIVO" %}
|
||||
<select id="{{ campo }}" name="{{ campo }}">
|
||||
<option value="1" {% if (dados[campo] if dados else "1")|string == "1" %}selected{% endif %}>Ativo</option>
|
||||
<option value="0" {% if (dados[campo] if dados else "1")|string == "0" %}selected{% endif %}>Inativo</option>
|
||||
</select>
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user