Add 'gerenciador/' from commit '34cc9c75a57dedbf61a6e6a1ba1f35f76e72434b'

git-subtree-dir: gerenciador
git-subtree-mainline: f4f546c928
git-subtree-split: 34cc9c75a5
This commit is contained in:
gti
2026-09-29 14:43:12 -03:00
84 changed files with 8906 additions and 0 deletions
+229
View File
@@ -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 %}