Cadastro da alíquota do ICMS correta. Inclusão da nova alíquota e comparação em todos os relatórios.
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-08-11 18:45:57 -03:00
parent 950eb2a826
commit 4d2fcff4a8
5 changed files with 368 additions and 68 deletions

View File

@@ -29,6 +29,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_session
from fastapi import Query
from sqlalchemy import select as sqla_select
from app.models import AliquotaUF
import pandas as pd
app = FastAPI()
@@ -40,29 +42,25 @@ UPLOAD_DIR = os.path.join("app", "uploads", "temp")
os.makedirs(UPLOAD_DIR, exist_ok=True)
def _parse_referencia(ref: str):
"""Aceita 'JAN/2024', 'JAN/24', '01/2024', '01/24', '202401'. Retorna (ano, mes)."""
"""Aceita 'JAN/2024', '01/2024', '202401' etc. Retorna (ano, mes)."""
meses = {'JAN':1,'FEV':2,'MAR':3,'ABR':4,'MAI':5,'JUN':6,'JUL':7,'AGO':8,'SET':9,'OUT':10,'NOV':11,'DEZ':12}
ref = (ref or "").strip().upper()
if "/" in ref:
a, b = [p.strip() for p in ref.split("/", 1)]
# mês pode vir 'JAN' ou '01'
mes = meses.get(a, None)
if mes is None:
mes = int(re.sub(r"\D", "", a) or 1)
ano = int(re.sub(r"\D", "", b) or 0)
# ano 2 dígitos -> 2000+
if ano < 100:
ano += 2000
else:
# '202401' ou '2024-01'
num = re.sub(r"\D", "", ref)
if len(num) >= 6:
ano, mes = int(num[:4]), int(num[4:6])
elif len(num) == 4: # '2024'
elif len(num) == 4:
ano, mes = int(num), 1
else:
ano, mes = date.today().year, 1
ano, mes = 0, 0
return ano, mes
async def _carregar_selic_map(session):
@@ -373,46 +371,59 @@ async def clear_all():
@app.get("/export-excel")
async def export_excel(
tipo: str = Query("geral", regex="^(geral|exclusao_icms|aliquota_icms)$"),
tipo: str = Query("geral", pattern="^(geral|exclusao_icms|aliquota_icms)$"),
cliente: str | None = Query(None)
):
import pandas as pd
# 1) Carregar faturas (com filtro por cliente, se houver)
async with AsyncSessionLocal() as session:
# 1) Faturas
stmt = select(Fatura)
if cliente:
# filtra por cliente_id (UUID em string)
stmt = stmt.where(Fatura.cliente_id == cliente)
faturas_result = await session.execute(stmt)
faturas = faturas_result.scalars().all()
faturas = (await session.execute(stmt)).scalars().all()
# 2) Mapa de alíquotas cadastradas (UF/ano)
aliq_rows = (await session.execute(select(AliquotaUF))).scalars().all()
aliq_map = {(r.uf.upper(), int(r.exercicio)): float(r.aliq_icms) for r in aliq_rows}
# 2) Montar dados conforme 'tipo'
dados = []
if tipo == "aliquota_icms":
# Campos: (Cliente, UC, Referência, Valor Total, ICMS (%), ICMS (R$),
# Base ICMS (R$), Consumo (kWh), Tarifa, Nota Fiscal)
for f in faturas:
uf = (f.estado or "").strip().upper()
ano, _ = _parse_referencia(f.referencia or "")
aliq_nf = float(f.icms_aliq or 0.0)
aliq_cad = aliq_map.get((uf, ano))
diff_pp = (aliq_nf - aliq_cad) if aliq_cad is not None else None
confere = (abs(diff_pp) < 1e-6) if diff_pp is not None else None
dados.append({
"Cliente": f.nome,
"UC": f.unidade_consumidora,
"UF (fatura)": uf,
"Exercício (ref)": ano,
"Referência": f.referencia,
"Valor Total": f.valor_total,
"ICMS (%)": f.icms_aliq,
"ICMS (R$)": f.icms_valor,
"Base ICMS (R$)": f.icms_base,
"Consumo (kWh)": f.consumo,
"Tarifa": f.tarifa,
"Nota Fiscal": f.nota_fiscal,
"ICMS (%) NF": aliq_nf,
# novas colunas padronizadas
"ICMS (%) (UF/Ref)": aliq_cad,
"Dif. ICMS (pp)": diff_pp,
"ICMS confere?": "SIM" if confere else ("N/D" if confere is None else "NÃO"),
"Valor Total": f.valor_total,
"Distribuidora": f.distribuidora,
"Data Processamento": f.data_processamento,
})
filename = "relatorio_aliquota_icms.xlsx"
elif tipo == "exclusao_icms":
# Campos: (Cliente, UC, Referência, Valor Total, PIS (%), ICMS (%), COFINS (%),
# PIS (R$), ICMS (R$), COFINS (R$), Base PIS (R$), Base ICMS (R$),
# Base COFINS (R$), Consumo (kWh), Tarifa, Nota Fiscal)
for f in faturas:
uf = (f.estado or "").strip().upper()
ano, _ = _parse_referencia(f.referencia or "")
aliq_nf = float(f.icms_aliq or 0.0)
aliq_cad = aliq_map.get((uf, ano))
diff_pp = (aliq_nf - aliq_cad) if aliq_cad is not None else None
confere = (abs(diff_pp) < 1e-6) if diff_pp is not None else None
dados.append({
"Cliente": f.nome,
"UC": f.unidade_consumidora,
@@ -427,17 +438,27 @@ async def export_excel(
"Base PIS (R$)": f.pis_base,
"Base ICMS (R$)": f.icms_base,
"Base COFINS (R$)": f.cofins_base,
# novas colunas
"ICMS (%) (UF/Ref)": aliq_cad,
"Dif. ICMS (pp)": diff_pp,
"ICMS confere?": "SIM" if confere else ("N/D" if confere is None else "NÃO"),
"Consumo (kWh)": f.consumo,
"Tarifa": f.tarifa,
"Nota Fiscal": f.nota_fiscal,
})
filename = "relatorio_exclusao_icms.xlsx"
else: # "geral" (mantém seu relatório atual — sem fórmulas SELIC)
# Se quiser manter exatamente o que já tinha com SELIC e fórmulas,
# você pode copiar sua lógica anterior aqui. Abaixo deixo um "geral"
# simplificado com as colunas principais.
else: # geral
for f in faturas:
uf = (f.estado or "").strip().upper()
ano, _ = _parse_referencia(f.referencia or "")
aliq_nf = float(f.icms_aliq or 0.0)
aliq_cad = aliq_map.get((uf, ano))
diff_pp = (aliq_nf - aliq_cad) if aliq_cad is not None else None
confere = (abs(diff_pp) < 1e-6) if diff_pp is not None else None
dados.append({
"Cliente": f.nome,
"UC": f.unidade_consumidora,
@@ -446,6 +467,12 @@ async def export_excel(
"Valor Total": f.valor_total,
"ICMS (%)": f.icms_aliq,
"ICMS (R$)": f.icms_valor,
# novas colunas
"ICMS (%) (UF/Ref)": aliq_cad,
"Dif. ICMS (pp)": diff_pp,
"ICMS confere?": "SIM" if confere else ("N/D" if confere is None else "NÃO"),
"Base ICMS (R$)": f.icms_base,
"PIS (%)": f.pis_aliq,
"PIS (R$)": f.pis_valor,
@@ -460,15 +487,13 @@ async def export_excel(
})
filename = "relatorio_geral.xlsx"
# 3) Gerar excel em memória
from io import BytesIO
# 3) Excel em memória
output = BytesIO()
df = pd.DataFrame(dados)
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
df.to_excel(writer, index=False, sheet_name="Relatório")
output.seek(0)
# 4) Responder
return StreamingResponse(
output,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
@@ -566,10 +591,12 @@ async def api_relatorios(
if cliente:
params["cliente"] = cliente
# ❗ Inclua 'estado' no SELECT
sql = text(f"""
SELECT id, nome, unidade_consumidora, referencia, nota_fiscal,
valor_total, icms_aliq, icms_valor, pis_aliq, pis_valor,
cofins_aliq, cofins_valor, distribuidora, data_processamento
cofins_aliq, cofins_valor, distribuidora, data_processamento,
estado
FROM faturas.faturas
{where}
ORDER BY data_processamento DESC
@@ -580,14 +607,27 @@ async def api_relatorios(
rows = (await db.execute(sql, params)).mappings().all()
total = (await db.execute(count_sql, params)).scalar_one()
items = [{
# 🔹 Carrega mapa de alíquotas UF/ano
aliq_rows = (await db.execute(select(AliquotaUF))).scalars().all()
aliq_map = {(r.uf.upper(), int(r.exercicio)): float(r.aliq_icms) for r in aliq_rows}
items = []
for r in rows:
uf = (r["estado"] or "").strip().upper()
ano, _mes = _parse_referencia(r["referencia"] or "")
aliq_nf = float(r["icms_aliq"] or 0.0)
aliq_cad = aliq_map.get((uf, ano))
diff_pp = (aliq_nf - aliq_cad) if aliq_cad is not None else None
ok = (abs(diff_pp) < 1e-6) if diff_pp is not None else None
items.append({
"id": str(r["id"]),
"nome": r["nome"],
"unidade_consumidora": r["unidade_consumidora"],
"referencia": r["referencia"],
"nota_fiscal": r["nota_fiscal"],
"valor_total": float(r["valor_total"]) if r["valor_total"] is not None else None,
"icms_aliq": r["icms_aliq"],
"icms_aliq": aliq_nf,
"icms_valor": r["icms_valor"],
"pis_aliq": r["pis_aliq"],
"pis_valor": r["pis_valor"],
@@ -595,6 +635,19 @@ async def api_relatorios(
"cofins_valor": r["cofins_valor"],
"distribuidora": r["distribuidora"],
"data_processamento": r["data_processamento"].isoformat() if r["data_processamento"] else None,
} for r in rows]
# novos
"estado": uf,
"exercicio": ano,
"aliq_cadastral": aliq_cad,
"aliq_diff_pp": round(diff_pp, 4) if diff_pp is not None else None,
"aliq_ok": ok,
})
return {"items": items, "total": total, "page": page, "page_size": page_size}
async def _carregar_aliquota_map(session):
rows = (await session.execute(
text("SELECT uf, exercicio, aliq_icms FROM faturas.aliquotas_uf")
)).mappings().all()
# (UF, ANO) -> float
return {(r["uf"].upper(), int(r["exercicio"])): float(r["aliq_icms"]) for r in rows}

View File

@@ -72,7 +72,7 @@ class AliquotaUF(Base):
id = Column(Integer, primary_key=True, autoincrement=True)
uf = Column(String)
exercicio = Column(String)
exercicio = Column(Integer)
aliq_icms = Column(Numeric(6, 4))
class SelicMensal(Base):

View File

@@ -1,5 +1,5 @@
# parametros.py
from fastapi import APIRouter, Request, Depends, Form
from fastapi import APIRouter, Request, Depends, Form, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_session
from app.models import AliquotaUF, ParametrosFormula, SelicMensal
@@ -9,7 +9,7 @@ import datetime
from fastapi.templating import Jinja2Templates
from sqlalchemy.future import select
from app.database import AsyncSessionLocal
from fastapi.responses import RedirectResponse
from fastapi.responses import RedirectResponse, JSONResponse
from app.models import Fatura
from fastapi import Body
from app.database import engine
@@ -21,6 +21,8 @@ import csv
from fastapi.responses import StreamingResponse
import pandas as pd
from io import BytesIO
from sqlalchemy import select
from decimal import Decimal
router = APIRouter()
@@ -100,6 +102,28 @@ async def editar_parametro(param_id: int, request: Request):
return {"success": True}
return {"success": False, "error": "Não encontrado"}
@router.post("/parametros/ativar/{param_id}")
async def ativar_parametro(param_id: int, request: Request):
data = await request.json()
ativo = bool(data.get("ativo", True))
async with AsyncSessionLocal() as session:
param = await session.get(ParametrosFormula, param_id)
if not param:
return JSONResponse(status_code=404, content={"error": "Parâmetro não encontrado"})
param.ativo = ativo
await session.commit()
return {"success": True}
@router.get("/parametros/delete/{param_id}")
async def deletar_parametro(param_id: int):
async with AsyncSessionLocal() as session:
param = await session.get(ParametrosFormula, param_id)
if not param:
return RedirectResponse("/parametros?erro=1&msg=Parâmetro não encontrado", status_code=303)
await session.delete(param)
await session.commit()
return RedirectResponse("/parametros?ok=1&msg=Parâmetro removido", status_code=303)
@router.post("/parametros/testar")
async def testar_formula(db: AsyncSession = Depends(get_session), data: dict = Body(...)):
formula = data.get("formula")
@@ -117,10 +141,18 @@ async def testar_formula(db: AsyncSession = Depends(get_session), data: dict = B
return {"success": False, "error": str(e)}
@router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema])
async def listar_aliquotas(db: AsyncSession = Depends(get_session)):
result = await db.execute(select(AliquotaUF).order_by(AliquotaUF.uf, AliquotaUF.exercicio))
return result.scalars().all()
@router.get("/parametros/aliquotas")
async def listar_aliquotas(uf: str | None = None, db: AsyncSession = Depends(get_session)):
stmt = select(AliquotaUF).order_by(AliquotaUF.uf, AliquotaUF.exercicio.desc())
if uf:
stmt = stmt.where(AliquotaUF.uf == uf)
rows = (await db.execute(stmt)).scalars().all()
return [
{"uf": r.uf, "exercicio": int(r.exercicio), "aliquota": float(r.aliq_icms)}
for r in rows
]
@router.post("/parametros/aliquotas")
async def adicionar_aliquota(aliq: AliquotaUFSchema, db: AsyncSession = Depends(get_session)):
@@ -227,5 +259,88 @@ def baixar_template_excel():
headers={"Content-Disposition": "attachment; filename=template_aliquotas.xlsx"}
)
@router.post("/parametros/aliquotas/salvar")
async def salvar_aliquota(payload: dict, db: AsyncSession = Depends(get_session)):
uf = (payload.get("uf") or "").strip().upper()
exercicio = int(payload.get("exercicio") or 0)
aliquota = Decimal(str(payload.get("aliquota") or "0"))
orig_uf = (payload.get("original_uf") or "").strip().upper() or uf
orig_ex = int(payload.get("original_exercicio") or 0) or exercicio
if not uf or not exercicio or aliquota <= 0:
return JSONResponse(status_code=400, content={"error": "UF, exercício e alíquota são obrigatórios."})
# busca pelo registro original (antes da edição)
stmt = select(AliquotaUF).where(
AliquotaUF.uf == orig_uf,
AliquotaUF.exercicio == orig_ex
)
existente = (await db.execute(stmt)).scalar_one_or_none()
if existente:
# atualiza (inclusive a chave, se mudou)
existente.uf = uf
existente.exercicio = exercicio
existente.aliq_icms = aliquota
else:
# não existia o original -> upsert padrão
db.add(AliquotaUF(uf=uf, exercicio=exercicio, aliq_icms=aliquota))
await db.commit()
return {"success": True}
@router.post("/parametros/aliquotas/importar")
async def importar_aliquotas_csv(arquivo: UploadFile = File(...), db: AsyncSession = Depends(get_session)):
content = await arquivo.read()
text = content.decode("utf-8", errors="ignore")
# tenta ; depois ,
sniffer = csv.Sniffer()
dialect = sniffer.sniff(text.splitlines()[0] if text else "uf;exercicio;aliquota")
reader = csv.DictReader(io.StringIO(text), dialect=dialect)
count = 0
for row in reader:
uf = (row.get("uf") or row.get("UF") or "").strip().upper()
exercicio_str = (row.get("exercicio") or row.get("ano") or "").strip()
try:
exercicio = int(exercicio_str)
except Exception:
continue
aliquota_str = (row.get("aliquota") or row.get("aliq_icms") or "").replace(",", ".").strip()
if not uf or not exercicio or not aliquota_str:
continue
try:
aliquota = Decimal(aliquota_str)
except Exception:
continue
stmt = select(AliquotaUF).where(AliquotaUF.uf == uf, AliquotaUF.exercicio == exercicio)
existente = (await db.execute(stmt)).scalar_one_or_none()
if existente:
existente.aliq_icms = aliquota
else:
db.add(AliquotaUF(uf=uf, exercicio=exercicio, aliq_icms=aliquota))
count += 1
await db.commit()
return {"success": True, "qtd": count}
@router.delete("/parametros/aliquotas/{uf}/{exercicio}")
async def excluir_aliquota(uf: str, exercicio: int, db: AsyncSession = Depends(get_session)):
stmt = select(AliquotaUF).where(
AliquotaUF.uf == uf.upper(),
AliquotaUF.exercicio == exercicio
)
row = (await db.execute(stmt)).scalar_one_or_none()
if not row:
return JSONResponse(status_code=404, content={"error": "Registro não encontrado."})
await db.delete(row)
await db.commit()
return {"success": True}

View File

@@ -22,7 +22,7 @@
</div>
<div class="form-group">
<label for="aliquota_icms">Alíquota de ICMS (%):</label>
<input type="number" step="0.01" name="aliquota_icms" id="aliquota_icms" value="{{ parametros.aliquota_icms or '' }}" />
<input type="text" id="aliquota" name="aliquota" inputmode="decimal" pattern="[0-9]+([,][0-9]+)?" placeholder="Ex: 20,7487">
</div>
</div>
@@ -62,6 +62,7 @@
<hr style="margin-top: 2rem; margin-bottom: 1rem;">
<h3 style="margin-top: 2rem;">📋 Fórmulas Salvas</h3>
<div class="card-list">
{% if
{% for param in lista_parametros %}
<div class="param-card {{ 'ativo' if param.ativo else 'inativo' }}" id="card-{{ param.id }}">
<div style="display:flex; justify-content:space-between; align-items:center;">
@@ -124,7 +125,7 @@
<!-- ABA ALÍQUOTAS -->
<div id="aliquotas" class="tab-content">
<div class="formulario-box">
<form onsubmit="return salvarAliquota(this)">
<form onsubmit="return salvarAliquota(this, event)">
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));">
<div class="form-group">
<label>UF:</label>
@@ -141,7 +142,10 @@
</div>
<div class="form-group">
<label>Alíquota ICMS (%):</label>
<input name="aliquota" type="number" step="0.0001" required />
<input id="aliquota-uf" name="aliquota"
type="text" inputmode="decimal"
pattern="[0-9]+([,][0-9]+)?"
placeholder="Ex: 20,7487" required />
</div>
</div>
<!-- Bloco com espaçamento e alinhamento central -->
@@ -160,12 +164,35 @@
</div>
</div>
<input type="hidden" id="orig-uf" name="original_uf">
<input type="hidden" id="orig-exercicio" name="original_exercicio">
</form>
<table class="selic-table">
<thead><tr><th>UF</th><th>Exercício</th><th>Alíquota</th></tr></thead>
<!-- Filtro de UF para a tabela -->
<div style="display:flex; align-items:center; gap:12px; margin:14px 0;">
<label for="filtro-uf" style="font-weight:600;">Filtrar por UF:</label>
<select id="filtro-uf" style="min-width:220px; padding:.5rem .75rem; border:1px solid #ddd; border-radius:8px;">
<option value="">Todas</option>
{% for uf in ['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO'] %}
<option value="{{ uf }}">{{ uf }}</option>
{% endfor %}
</select>
<span id="total-aliquotas" class="muted"></span>
</div>
<table class="selic-table">
<thead>
<tr>
<th>UF</th>
<th>Exercício</th>
<th>Alíquota</th>
<th style="width:140px;">Ações</th>
</tr>
</thead>
<tbody id="tabela-aliquotas"></tbody>
</table>
</table>
</div>
</div>
@@ -581,16 +608,39 @@
// ✅ Carrega tabela de alíquotas
async function carregarAliquotas() {
const res = await fetch("/parametros/aliquotas");
const uf = document.getElementById("filtro-uf")?.value || "";
const url = new URL("/parametros/aliquotas", window.location.origin);
if (uf) url.searchParams.set("uf", uf);
const res = await fetch(url);
const dados = await res.json();
const tbody = document.getElementById("tabela-aliquotas");
if (!dados.length) {
tbody.innerHTML = `<tr><td colspan="3" style="padding:.6rem;">Nenhum registro.</td></tr>`;
} else {
tbody.innerHTML = dados.map(a => `
<tr><td>${a.uf}</td><td>${a.exercicio}</td><td>${a.aliquota.toFixed(4)}%</td></tr>
<tr>
<td>${a.uf}</td>
<td>${a.exercicio}</td>
<td>${Number(a.aliquota).toLocaleString('pt-BR', {minimumFractionDigits:4, maximumFractionDigits:4})}%</td>
<td style="display:flex; gap:8px;">
<button class="btn btn-sm btn-secondary"
onclick="editarAliquota('${a.uf}', ${a.exercicio}, ${Number(a.aliquota)})">✏️ Editar</button>
<button class="btn btn-sm btn-danger"
onclick="excluirAliquota('${a.uf}', ${a.exercicio})">🗑️ Excluir</button>
</td>
</tr>
`).join('');
}
document.getElementById("total-aliquotas").textContent = `Registros: ${dados.length}`;
}
// ✅ Eventos após carregar DOM
document.addEventListener('DOMContentLoaded', () => {
document.getElementById("filtro-uf")?.addEventListener("change", carregarAliquotas);
carregarAliquotas();
// Ativar/desativar checkbox
@@ -657,6 +707,86 @@
});
}
async function salvarAliquota(form, ev) {
ev?.preventDefault();
const uf = form.uf.value?.trim();
const exercicio = Number(form.exercicio.value?.trim());
const aliquotaStr = form.aliquota.value?.trim();
const aliquota = parseFloat(aliquotaStr.replace(',', '.')); // vírgula -> ponto
// 👇 LE OS ORIGINAIS
const original_uf = document.getElementById('orig-uf').value || null;
const original_exercicio = document.getElementById('orig-exercicio').value
? Number(document.getElementById('orig-exercicio').value)
: null;
if (!uf || !exercicio || isNaN(exercicio) || !aliquotaStr || isNaN(aliquota)) {
mostrarFeedback("❌ Erro", "Preencha UF, exercício e alíquota válidos.");
return false;
}
const res = await fetch("/parametros/aliquotas/salvar", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ uf, exercicio, aliquota, original_uf, original_exercicio })
});
if (!res.ok) {
const msg = await res.text();
mostrarFeedback("❌ Erro ao salvar", msg || "Falha na operação.");
return false;
}
mostrarFeedback("✅ Salvo", "Alíquota registrada/atualizada com sucesso.");
cancelarEdicao(); // limpa modo edição
carregarAliquotas();
return false;
}
function editarAliquota(uf, exercicio, aliquota) {
const form = document.querySelector('#aliquotas form');
form.uf.value = uf;
form.exercicio.value = String(exercicio);
// Mostrar no input com vírgula e 4 casas
const valorBR = Number(aliquota).toLocaleString('pt-BR', {
minimumFractionDigits: 4, maximumFractionDigits: 4
});
form.querySelector('[name="aliquota"]').value = valorBR;
// 👇 GUARDA A CHAVE ORIGINAL
document.getElementById('orig-uf').value = uf;
document.getElementById('orig-exercicio').value = String(exercicio);
document.getElementById('aliquotas')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function cancelarEdicao(){
const form = document.querySelector('#aliquotas form');
form.reset();
document.getElementById('orig-uf').value = '';
document.getElementById('orig-exercicio').value = '';
}
async function excluirAliquota(uf, exercicio){
if(!confirm(`Excluir a alíquota de ${uf}/${exercicio}?`)) return;
const res = await fetch(`/parametros/aliquotas/${encodeURIComponent(uf)}/${exercicio}`, {
method: 'DELETE'
});
if(!res.ok){
const msg = await res.text();
mostrarFeedback("❌ Erro", msg || "Falha ao excluir.");
return;
}
mostrarFeedback("🗑️ Excluída", "Alíquota removida com sucesso.");
carregarAliquotas();
}
</script>
<!-- Feedback estilo popup -->

View File

@@ -32,8 +32,10 @@
<a class="btn btn-secondary" href="/erros/log">📄 Ver Log de Erros (.txt)</a>
</div>
{% endif %}
<!--
<button class="btn btn-success" onclick="baixarPlanilha()">📅 Abrir Planilha</button>
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button>
-->
{% if app_env != "producao" %}
<button class="btn btn-warning" onclick="limparFaturas()">🧹 Limpar Faturas (Teste)</button>
{% endif %}