Atualização: template Excel de alíquotas e layout da aba
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-07-30 09:48:44 -03:00
parent b51eeac014
commit d8db2a60e5
8 changed files with 976 additions and 198 deletions

View File

@@ -3,16 +3,16 @@ from sqlalchemy.orm import sessionmaker, declarative_base
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
from collections.abc import AsyncGenerator
load_dotenv() load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL") DATABASE_URL = os.getenv("DATABASE_URL")
async_engine = create_async_engine(DATABASE_URL, echo=False, future=True) engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = sessionmaker(bind=async_engine, class_=AsyncSession, expire_on_commit=False) AsyncSessionLocal = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False)
Base = declarative_base() Base = declarative_base()
@asynccontextmanager async def get_session() -> AsyncGenerator[AsyncSession, None]:
async def get_session():
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
yield session yield session

View File

@@ -1,11 +1,11 @@
import asyncio
import uuid import uuid
from fastapi import FastAPI, Request, UploadFile, File from fastapi import FastAPI, HTTPException, Request, UploadFile, File
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import os, shutil import os, shutil
from sqlalchemy import text from sqlalchemy import text
from fastapi import Depends
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from io import BytesIO from io import BytesIO
import pandas as pd import pandas as pd
@@ -20,6 +20,12 @@ from app.processor import (
limpar_arquivos_processados limpar_arquivos_processados
) )
from app.parametros import router as parametros_router from app.parametros import router as parametros_router
from fastapi.responses import FileResponse
from app.models import Fatura, SelicMensal, ParametrosFormula
from datetime import date
from app.utils import avaliar_formula
app = FastAPI() app = FastAPI()
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
@@ -61,16 +67,6 @@ def upload_page(request: Request):
def relatorios_page(request: Request): def relatorios_page(request: Request):
return templates.TemplateResponse("relatorios.html", {"request": request}) return templates.TemplateResponse("relatorios.html", {"request": request})
@app.get("/parametros", response_class=HTMLResponse)
async def parametros_page(request: Request):
async with AsyncSessionLocal() as session:
result = await session.execute(select(ParametrosFormula))
parametros = result.scalars().first()
return templates.TemplateResponse("parametros.html", {
"request": request,
"parametros": parametros or {}
})
@app.post("/upload-files") @app.post("/upload-files")
async def upload_files(files: list[UploadFile] = File(...)): async def upload_files(files: list[UploadFile] = File(...)):
for file in files: for file in files:
@@ -123,11 +119,72 @@ async def clear_all():
@app.get("/export-excel") @app.get("/export-excel")
async def export_excel(): async def export_excel():
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
result = await session.execute(select(Fatura)) # 1. Coletar faturas e tabela SELIC
faturas = result.scalars().all() faturas_result = await session.execute(select(Fatura))
faturas = faturas_result.scalars().all()
selic_result = await session.execute(select(SelicMensal))
selic_tabela = selic_result.scalars().all()
# 2. Criar mapa {(ano, mes): percentual}
selic_map = {(s.ano, s.mes): float(s.percentual) for s in selic_tabela}
hoje = date.today()
def calcular_fator_selic(ano_inicio, mes_inicio):
fator = 1.0
ano, mes = ano_inicio, mes_inicio
while (ano < hoje.year) or (ano == hoje.year and mes <= hoje.month):
percentual = selic_map.get((ano, mes))
if percentual:
fator *= (1 + percentual / 100)
mes += 1
if mes > 12:
mes = 1
ano += 1
return fator
# 3. Buscar fórmulas exatas por nome
formula_pis_result = await session.execute(
select(ParametrosFormula.formula).where(
ParametrosFormula.nome == "Cálculo PIS sobre ICMS",
ParametrosFormula.ativo == True
).limit(1)
)
formula_cofins_result = await session.execute(
select(ParametrosFormula.formula).where(
ParametrosFormula.nome == "Cálculo COFINS sobre ICMS",
ParametrosFormula.ativo == True
).limit(1)
)
formula_pis = formula_pis_result.scalar_one_or_none()
formula_cofins = formula_cofins_result.scalar_one_or_none()
# 4. Montar dados
mes_map = {
'JAN': 1, 'FEV': 2, 'MAR': 3, 'ABR': 4, 'MAI': 5, 'JUN': 6,
'JUL': 7, 'AGO': 8, 'SET': 9, 'OUT': 10, 'NOV': 11, 'DEZ': 12
}
dados = [] dados = []
for f in faturas: for f in faturas:
try:
if "/" in f.referencia:
mes_str, ano_str = f.referencia.split("/")
mes = mes_map.get(mes_str.strip().upper())
ano = int(ano_str)
if not mes or not ano:
raise ValueError("Mês ou ano inválido")
else:
ano = int(f.referencia[:4])
mes = int(f.referencia[4:])
fator = calcular_fator_selic(ano, mes)
periodo = f"{mes:02d}/{ano} à {hoje.month:02d}/{hoje.year}"
contexto = f.__dict__
valor_pis_icms = avaliar_formula(formula_pis, contexto) if formula_pis else None
valor_cofins_icms = avaliar_formula(formula_cofins, contexto) if formula_cofins else None
dados.append({ dados.append({
"Nome": f.nome, "Nome": f.nome,
"UC": f.unidade_consumidora, "UC": f.unidade_consumidora,
@@ -149,18 +206,27 @@ async def export_excel():
"Estado": f.estado, "Estado": f.estado,
"Distribuidora": f.distribuidora, "Distribuidora": f.distribuidora,
"Data Processamento": f.data_processamento, "Data Processamento": f.data_processamento,
"Fator SELIC acumulado": fator,
"Período SELIC usado": periodo,
"PIS sobre ICMS": valor_pis_icms,
"Valor Corrigido PIS (ICMS)": valor_pis_icms * fator if valor_pis_icms else None,
"COFINS sobre ICMS": valor_cofins_icms,
"Valor Corrigido COFINS (ICMS)": valor_cofins_icms * fator if valor_cofins_icms else None,
}) })
except Exception as e:
print(f"Erro ao processar fatura {f.nota_fiscal}: {e}")
df = pd.DataFrame(dados) df = pd.DataFrame(dados)
output = BytesIO()
df.to_excel(output, index=False, sheet_name="Faturas")
output.seek(0)
return StreamingResponse( output = BytesIO()
output, with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", df.to_excel(writer, index=False, sheet_name="Faturas Corrigidas")
headers={"Content-Disposition": "attachment; filename=relatorio_faturas.xlsx"}
) output.seek(0)
return StreamingResponse(output, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={
"Content-Disposition": "attachment; filename=faturas_corrigidas.xlsx"
})
from app.parametros import router as parametros_router from app.parametros import router as parametros_router
app.include_router(parametros_router) app.include_router(parametros_router)
@@ -186,3 +252,33 @@ async def limpar_faturas():
os.remove(caminho) os.remove(caminho)
return {"message": "Faturas e arquivos apagados com sucesso."} return {"message": "Faturas e arquivos apagados com sucesso."}
@app.get("/erros/download")
async def download_erros():
zip_path = os.path.join("app", "uploads", "erros", "faturas_erro.zip")
if os.path.exists(zip_path):
response = FileResponse(zip_path, filename="faturas_erro.zip", media_type="application/zip")
# ⚠️ Agendar exclusão após resposta
asyncio.create_task(limpar_erros())
return response
else:
raise HTTPException(status_code=404, detail="Arquivo de erro não encontrado.")
@app.get("/erros/log")
async def download_log_erros():
txt_path = os.path.join("app", "uploads", "erros", "erros.txt")
if os.path.exists(txt_path):
response = FileResponse(txt_path, filename="erros.txt", media_type="text/plain")
# ⚠️ Agendar exclusão após resposta
asyncio.create_task(limpar_erros())
return response
else:
raise HTTPException(status_code=404, detail="Log de erro não encontrado.")
async def limpar_erros():
await asyncio.sleep(5) # Aguarda 5 segundos para garantir que o download inicie
pasta = os.path.join("app", "uploads", "erros")
for nome in ["faturas_erro.zip", "erros.txt"]:
caminho = os.path.join(pasta, nome)
if os.path.exists(caminho):
os.remove(caminho)

View File

@@ -5,20 +5,17 @@ import uuid
from datetime import datetime from datetime import datetime
from app.database import Base from app.database import Base
from sqlalchemy import Boolean from sqlalchemy import Boolean
from sqlalchemy import Column, Integer, String, Numeric
class ParametrosFormula(Base): class ParametrosFormula(Base):
__tablename__ = "parametros_formula" __tablename__ = "parametros_formula"
__table_args__ = {"schema": "faturas"} __table_args__ = {"schema": "faturas"}
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True)
tipo = Column(String(20)) nome = Column(String(50))
formula = Column(Text) formula = Column(Text)
ativo = Column(Boolean) ativo = Column(Boolean, default=True)
aliquota_icms = Column(Float)
incluir_icms = Column(Integer)
incluir_pis = Column(Integer)
incluir_cofins = Column(Integer)
class Fatura(Base): class Fatura(Base):
__tablename__ = "faturas" __tablename__ = "faturas"
@@ -74,13 +71,12 @@ class AliquotaUF(Base):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
uf = Column(String) uf = Column(String)
exercicio = Column(String) exercicio = Column(String)
aliquota = Column(Float) aliq_icms = Column(Numeric(6, 4))
class SelicMensal(Base): class SelicMensal(Base):
__tablename__ = "selic_mensal" __tablename__ = "selic_mensal"
__table_args__ = {'schema': 'faturas'} __table_args__ = {'schema': 'faturas'}
id = Column(Integer, primary_key=True, autoincrement=True) ano = Column(Integer, primary_key=True)
ano = Column(Integer) mes = Column(Integer, primary_key=True)
mes = Column(Integer) percentual = Column(Numeric(6, 4))
fator = Column(Float)

View File

@@ -1,6 +1,5 @@
# parametros.py # parametros.py
from fastapi import APIRouter, Request, HTTPException, Depends from fastapi import APIRouter, Request, Depends, Form
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_session from app.database import get_session
from app.models import AliquotaUF, ParametrosFormula, SelicMensal from app.models import AliquotaUF, ParametrosFormula, SelicMensal
@@ -10,6 +9,19 @@ import datetime
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlalchemy.future import select from sqlalchemy.future import select
from app.database import AsyncSessionLocal from app.database import AsyncSessionLocal
from fastapi.responses import RedirectResponse
from app.models import Fatura
from fastapi import Body
from app.database import engine
import httpx
from app.models import SelicMensal
from sqlalchemy.dialects.postgresql import insert as pg_insert
import io
import csv
from fastapi.responses import StreamingResponse
import pandas as pd
from io import BytesIO
router = APIRouter() router = APIRouter()
@@ -17,83 +29,202 @@ router = APIRouter()
class AliquotaUFSchema(BaseModel): class AliquotaUFSchema(BaseModel):
uf: str uf: str
exercicio: int exercicio: int
aliquota: float aliq_icms: float
class Config: class Config:
orm_mode = True from_attributes = True
class ParametrosFormulaSchema(BaseModel): class ParametrosFormulaSchema(BaseModel):
nome: str nome: str
formula: str formula: str
campos: str ativo: bool = True
class Config: class Config:
orm_mode = True from_attributes = True
class SelicMensalSchema(BaseModel): class SelicMensalSchema(BaseModel):
mes: str # 'YYYY-MM' mes: str # 'YYYY-MM'
fator: float fator: float
class Config: class Config:
orm_mode = True from_attributes = True
# === Rotas === # === Rotas ===
router = APIRouter()
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
@router.get("/parametros") @router.get("/parametros")
async def parametros_page(request: Request): async def parametros_page(request: Request):
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
result = await session.execute(select(ParametrosFormula).where(ParametrosFormula.ativo == True)) # Consulta das fórmulas
parametros = result.scalars().first() result = await session.execute(select(ParametrosFormula))
parametros = result.scalars().all()
# Consulta da tabela selic_mensal
selic_result = await session.execute(
select(SelicMensal).order_by(SelicMensal.ano.desc(), SelicMensal.mes.desc())
)
selic_dados = selic_result.scalars().all()
# Pega última data
ultima_data_selic = "-"
if selic_dados:
ultima = selic_dados[0]
ultima_data_selic = f"{ultima.mes:02d}/{ultima.ano}"
# Campos numéricos da fatura
campos = [
col.name for col in Fatura.__table__.columns
if col.type.__class__.__name__ in ['Integer', 'Float', 'Numeric']
]
return templates.TemplateResponse("parametros.html", { return templates.TemplateResponse("parametros.html", {
"request": request, "request": request,
"parametros": parametros or {} "lista_parametros": parametros,
"parametros": {},
"campos_fatura": campos,
"selic_dados": selic_dados,
"ultima_data_selic": ultima_data_selic
}) })
@router.post("/parametros/editar/{param_id}")
async def editar_parametro(param_id: int, request: Request):
data = await request.json()
async with AsyncSessionLocal() as session:
param = await session.get(ParametrosFormula, param_id)
if param:
param.tipo = data.get("tipo", param.tipo)
param.formula = data.get("formula", param.formula)
await session.commit()
return {"success": True}
return {"success": False, "error": "Não encontrado"}
@router.post("/parametros/testar")
async def testar_formula(db: AsyncSession = Depends(get_session), data: dict = Body(...)):
formula = data.get("formula")
exemplo = await db.execute(select(Fatura).limit(1))
fatura = exemplo.scalar_one_or_none()
if not fatura:
return {"success": False, "error": "Sem dados para teste."}
try:
contexto = {col.name: getattr(fatura, col.name) for col in Fatura.__table__.columns}
resultado = eval(formula, {}, contexto)
return {"success": True, "resultado": resultado}
except Exception as e:
return {"success": False, "error": str(e)}
@router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema]) @router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema])
def listar_aliquotas(db: AsyncSession = Depends(get_session)): async def listar_aliquotas(db: AsyncSession = Depends(get_session)):
return db.query(AliquotaUF).all() result = await db.execute(select(AliquotaUF).order_by(AliquotaUF.uf, AliquotaUF.exercicio))
return result.scalars().all()
@router.post("/parametros/aliquotas") @router.post("/parametros/aliquotas")
def adicionar_aliquota(aliq: AliquotaUFSchema, db: AsyncSession = Depends(get_session)): async def adicionar_aliquota(aliq: AliquotaUFSchema, db: AsyncSession = Depends(get_session)):
existente = db.query(AliquotaUF).filter_by(uf=aliq.uf, exercicio=aliq.exercicio).first() result = await db.execute(
select(AliquotaUF).filter_by(uf=aliq.uf, exercicio=aliq.exercicio)
)
existente = result.scalar_one_or_none()
if existente: if existente:
existente.aliquota = aliq.aliquota existente.aliq_icms = aliq.aliq_icms # atualizado
else: else:
novo = AliquotaUF(**aliq.dict()) novo = AliquotaUF(**aliq.dict())
db.add(novo) db.add(novo)
db.commit()
return {"status": "ok"} await db.commit()
return RedirectResponse(url="/parametros?ok=true&msg=Alíquota salva com sucesso", status_code=303)
@router.get("/parametros/formulas", response_model=List[ParametrosFormulaSchema]) @router.get("/parametros/formulas", response_model=List[ParametrosFormulaSchema])
def listar_formulas(db: AsyncSession = Depends(get_session)): async def listar_formulas(db: AsyncSession = Depends(get_session)):
return db.query(ParametrosFormula).all() result = await db.execute(select(ParametrosFormula).order_by(ParametrosFormula.tipo))
return result.scalars().all()
@router.post("/parametros/formulas") @router.post("/parametros/formulas")
def salvar_formula(form: ParametrosFormulaSchema, db: AsyncSession = Depends(get_session)): async def salvar_formula(form: ParametrosFormulaSchema, db: AsyncSession = Depends(get_session)):
existente = db.query(ParametrosFormula).filter_by(nome=form.nome).first() result = await db.execute(
select(ParametrosFormula).filter_by(tipo=form.tipo)
)
existente = result.scalar_one_or_none()
if existente: if existente:
existente.formula = form.formula existente.formula = form.formula
existente.campos = form.campos existente.campos = form.campos
else: else:
novo = ParametrosFormula(**form.dict()) novo = ParametrosFormula(**form.dict())
db.add(novo) db.add(novo)
db.commit()
return {"status": "ok"} await db.commit()
return RedirectResponse(url="/parametros?ok=true&msg=Parâmetro salvo com sucesso", status_code=303)
@router.get("/parametros/selic", response_model=List[SelicMensalSchema]) @router.get("/parametros/selic", response_model=List[SelicMensalSchema])
def listar_selic(db: AsyncSession = Depends(get_session)): async def listar_selic(db: AsyncSession = Depends(get_session)):
return db.query(SelicMensal).order_by(SelicMensal.mes.desc()).all() result = await db.execute(select(SelicMensal).order_by(SelicMensal.mes.desc()))
return result.scalars().all()
@router.post("/parametros/selic/importar")
async def importar_selic(request: Request, data_maxima: str = Form(None)):
try:
hoje = datetime.date.today()
inicio = datetime.date(hoje.year - 5, 1, 1)
fim = datetime.datetime.strptime(data_maxima, "%Y-%m-%d").date() if data_maxima else hoje
url = (
f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.4390/dados?"
f"formato=json&dataInicial={inicio.strftime('%d/%m/%Y')}&dataFinal={fim.strftime('%d/%m/%Y')}"
)
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status()
dados = response.json()
registros = []
for item in dados:
data = datetime.datetime.strptime(item['data'], "%d/%m/%Y")
ano, mes = data.year, data.month
percentual = float(item['valor'].replace(',', '.'))
registros.append({"ano": ano, "mes": mes, "percentual": percentual})
async with engine.begin() as conn:
stmt = pg_insert(SelicMensal.__table__).values(registros)
upsert_stmt = stmt.on_conflict_do_update(
index_elements=['ano', 'mes'],
set_={'percentual': stmt.excluded.percentual}
)
await conn.execute(upsert_stmt)
return RedirectResponse("/parametros?aba=selic", status_code=303)
except Exception as e:
return RedirectResponse(f"/parametros?erro=1&msg={str(e)}", status_code=303)
@router.get("/parametros/aliquotas/template")
def baixar_template_excel():
df = pd.DataFrame(columns=["UF", "Exercício", "Alíquota"])
df.loc[0] = ["SP", "2025", "18"] # exemplo opcional
df.loc[1] = ["MG", "2025", "12"] # exemplo opcional
output = BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, sheet_name='Template', index=False)
# Adiciona instrução como observação na célula A5 (linha 5)
sheet = writer.sheets['Template']
sheet.cell(row=5, column=1).value = (
"⚠️ Após preencher, salve como CSV (.csv separado por vírgulas) para importar no sistema."
)
output.seek(0)
return StreamingResponse(
output,
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={"Content-Disposition": "attachment; filename=template_aliquotas.xlsx"}
)
@router.post("/parametros/selic")
def salvar_selic(selic: SelicMensalSchema, db: AsyncSession = Depends(get_session)):
existente = db.query(SelicMensal).filter_by(mes=selic.mes).first()
if existente:
existente.fator = selic.fator
else:
novo = SelicMensal(**selic.dict())
db.add(novo)
db.commit()
return {"status": "ok"}

View File

@@ -2,6 +2,7 @@ import logging
import os import os
import shutil import shutil
import asyncio import asyncio
import httpx
from sqlalchemy.future import select from sqlalchemy.future import select
from app.utils import extrair_dados_pdf from app.utils import extrair_dados_pdf
from app.database import AsyncSessionLocal from app.database import AsyncSessionLocal
@@ -9,6 +10,9 @@ from app.models import Fatura, LogProcessamento
import time import time
import traceback import traceback
import uuid import uuid
from app.models import SelicMensal
from sqlalchemy import select
from zipfile import ZipFile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -28,6 +32,9 @@ def remover_arquivo_temp(caminho_pdf):
logger.warning(f"Falha ao remover arquivo temporário: {e}") logger.warning(f"Falha ao remover arquivo temporário: {e}")
def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal): def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
ERROS_DIR = os.path.join("app", "uploads", "erros")
os.makedirs(ERROS_DIR, exist_ok=True)
erros_detectados = []
try: try:
extensao = os.path.splitext(nome_original)[1].lower() extensao = os.path.splitext(nome_original)[1].lower()
nome_destino = f"{nota_fiscal}_{uuid.uuid4().hex[:6]}{extensao}" nome_destino = f"{nota_fiscal}_{uuid.uuid4().hex[:6]}{extensao}"
@@ -35,6 +42,17 @@ def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
shutil.copy2(caminho_pdf_temp, destino_final) shutil.copy2(caminho_pdf_temp, destino_final)
return destino_final return destino_final
except Exception as e: except Exception as e:
# Copiar o arquivo com erro
extensao = os.path.splitext(nome_original)[1].lower()
nome_arquivo = f"{uuid.uuid4().hex[:6]}_erro{extensao}"
caminho_pdf = caminho_pdf_temp
shutil.copy2(caminho_pdf, os.path.join(ERROS_DIR, nome_arquivo))
mensagem = f"{nome_arquivo}: {str(e)}"
erros_detectados.append(mensagem)
logger.error(f"Erro ao salvar em uploads: {e}") logger.error(f"Erro ao salvar em uploads: {e}")
return caminho_pdf_temp return caminho_pdf_temp
@@ -62,6 +80,10 @@ async def process_single_file(caminho_pdf_temp: str, nome_original: str):
"tempo": f"{duracao}s" "tempo": f"{duracao}s"
} }
data_comp = dados.get("competencia")
if data_comp:
await garantir_selic_para_competencia(session, data_comp.year, data_comp.month)
# Salva arquivo final # Salva arquivo final
caminho_final = salvar_em_uploads(caminho_pdf_temp, nome_original, dados['nota_fiscal']) caminho_final = salvar_em_uploads(caminho_pdf_temp, nome_original, dados['nota_fiscal'])
dados['link_arquivo'] = caminho_final dados['link_arquivo'] = caminho_final
@@ -97,7 +119,6 @@ async def process_single_file(caminho_pdf_temp: str, nome_original: str):
"trace": erro_str "trace": erro_str
} }
async def processar_em_lote(): async def processar_em_lote():
import traceback # para exibir erros import traceback # para exibir erros
resultados = [] resultados = []
@@ -128,9 +149,49 @@ async def processar_em_lote():
}) })
print(f"Erro ao processar {item['nome_original']}: {e}") print(f"Erro ao processar {item['nome_original']}: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
# Após o loop, salvar TXT com erros
erros_txt = []
for nome, status in status_arquivos.items():
if status['status'] == 'Erro':
erros_txt.append(f"{nome} - {status.get('mensagem', 'Erro desconhecido')}")
if erros_txt:
with open(os.path.join(UPLOADS_DIR, "erros", "erros.txt"), "w", encoding="utf-8") as f:
f.write("\n".join(erros_txt))
# Compacta PDFs com erro
with ZipFile(os.path.join(UPLOADS_DIR, "erros", "faturas_erro.zip"), "w") as zipf:
for nome in status_arquivos:
if status_arquivos[nome]['status'] == 'Erro':
caminho = os.path.join(UPLOADS_DIR, "temp", nome)
if os.path.exists(caminho):
zipf.write(caminho, arcname=nome)
return resultados return resultados
def limpar_arquivos_processados(): def limpar_arquivos_processados():
status_arquivos.clear() status_arquivos.clear()
while not fila_processamento.empty(): while not fila_processamento.empty():
fila_processamento.get_nowait() fila_processamento.get_nowait()
async def garantir_selic_para_competencia(session, ano, mes):
# Verifica se já existe
result = await session.execute(select(SelicMensal).filter_by(ano=ano, mes=mes))
existente = result.scalar_one_or_none()
if existente:
return # já tem
# Busca na API do Banco Central
url = (
f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.4390/dados?"
f"formato=json&dataInicial=01/{mes:02d}/{ano}&dataFinal=30/{mes:02d}/{ano}"
)
async with httpx.AsyncClient() as client:
resp = await client.get(url)
resp.raise_for_status()
dados = resp.json()
if dados:
percentual = float(dados[0]["valor"].replace(",", "."))
novo = SelicMensal(ano=ano, mes=mes, fator=percentual)
session.add(novo)
await session.commit()

View File

@@ -9,15 +9,16 @@
<div class="tabs"> <div class="tabs">
<button class="tab active" onclick="switchTab('formulas')">📄 Fórmulas</button> <button class="tab active" onclick="switchTab('formulas')">📄 Fórmulas</button>
<button class="tab" onclick="switchTab('selic')">📊 Gestão SELIC</button> <button class="tab" onclick="switchTab('selic')">📊 Gestão SELIC</button>
<button class="tab" onclick="switchTab('aliquotas')">🧾 Cadastro de Alíquotas por Estado</button>
</div> </div>
<!-- ABA FÓRMULAS --> <!-- ABA FÓRMULAS -->
<div id="formulas" class="tab-content active"> <div id="formulas" class="tab-content active">
<form method="post" class="formulario-box"> <form method="post" class="formulario-box">
<div class="grid"> <div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));">
<div class="form-group"> <div class="form-group">
<label for="tipo">Tipo:</label> <label for="nome">Nome:</label>
<input type="text" name="tipo" id="tipo" value="{{ parametros.tipo or '' }}" required /> <input type="text" name="nome" id="nome" value="{{ parametros.nome or '' }}" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="aliquota_icms">Alíquota de ICMS (%):</label> <label for="aliquota_icms">Alíquota de ICMS (%):</label>
@@ -28,14 +29,23 @@
<div class="form-group"> <div class="form-group">
<label for="formula">Fórmula:</label> <label for="formula">Fórmula:</label>
<div class="editor-box"> <div class="editor-box">
<select onchange="inserirCampo(this)"> <div style="margin-bottom: 0.5rem;">
<option value="">Inserir campo...</option> <strong>Campos disponíveis:</strong>
<option value="pis_base">pis_base</option> <div class="campo-badges">
<option value="cofins_base">cofins_base</option> {% for campo in campos_fatura %}
<option value="icms_valor">icms_valor</option> <span class="badge-campo" onclick="inserirNoEditor('{{ campo }}')">{{ campo }}</span>
<option value="pis_aliq">pis_aliq</option> {% endfor %}
<option value="cofins_aliq">cofins_aliq</option> </div>
</select> </div>
<div style="margin-bottom: 0.5rem;">
<strong>Operadores:</strong>
<div class="campo-badges">
{% for op in ['+', '-', '*', '/', '(', ')'] %}
<span class="badge-operador" onclick="inserirNoEditor('{{ op }}')">{{ op }}</span>
{% endfor %}
</div>
</div>
<textarea name="formula" id="formula" rows="3" required>{{ parametros.formula or '' }}</textarea> <textarea name="formula" id="formula" rows="3" required>{{ parametros.formula or '' }}</textarea>
<div class="actions-inline"> <div class="actions-inline">
<button type="button" class="btn btn-secondary" onclick="testarFormula()">🧪 Testar Fórmula</button> <button type="button" class="btn btn-secondary" onclick="testarFormula()">🧪 Testar Fórmula</button>
@@ -45,64 +55,149 @@
</div> </div>
</div> </div>
<div class="form-group check-group">
<label><input type="checkbox" name="incluir_icms" value="1" {% if parametros.incluir_icms %}checked{% endif %}> Incluir ICMS</label>
<label><input type="checkbox" name="incluir_pis" value="1" {% if parametros.incluir_pis %}checked{% endif %}> Incluir PIS</label>
<label><input type="checkbox" name="incluir_cofins" value="1" {% if parametros.incluir_cofins %}checked{% endif %}> Incluir COFINS</label>
<label><input type="checkbox" name="ativo" value="1" {% if parametros.ativo %}checked{% endif %}> Ativo</label>
</div>
<button type="submit" class="btn btn-primary">💾 Salvar Parâmetro</button> <button type="submit" class="btn btn-primary">💾 Salvar Parâmetro</button>
</form> <button type="button" class="btn btn-primary pulse" onclick="limparFormulario()">🔁 Novo Parâmetro</button>
</form>
<hr style="margin-top: 2rem; margin-bottom: 1rem;">
<h3 style="margin-top: 2rem;">📋 Fórmulas Salvas</h3> <h3 style="margin-top: 2rem;">📋 Fórmulas Salvas</h3>
<div class="card-list"> <div class="card-list">
{% for param in lista_parametros %} {% for param in lista_parametros %}
<div class="param-card"> <div class="param-card {{ 'ativo' if param.ativo else 'inativo' }}" id="card-{{ param.id }}">
<h4>{{ param.tipo }}</h4> <div style="display:flex; justify-content:space-between; align-items:center;">
<code>{{ param.formula }}</code> <input type="text" class="edit-nome" value="{{ param.nome }}" data-id="{{ param.id }}"
onkeydown="if(event.key==='Enter'){ event.preventDefault(); salvarInline('{{ param.id }}') }" />
<span class="badge-status">{{ 'Ativo ✅' if param.ativo else 'Inativo ❌' }}</span>
</div>
<textarea class="edit-formula" data-id="{{ param.id }}" title="{{ param.formula }}">{{ param.formula }}</textarea>
<!-- botão de testar e salvar -->
<div style="display: flex; justify-content: space-between; align-items:center;">
<label>
<input type="checkbox" class="toggle-ativo" data-id="{{ param.id }}" {% if param.ativo %}checked{% endif %}>
Ativo
</label>
<div class="actions"> <div class="actions">
<a href="?editar={{ param.id }}" class="btn btn-sm">✏️ Editar</a> <button type="button" class="btn btn-sm btn-secondary btn-testar" data-id="{{ param.id }}">🧪 Testar</button>
<a href="?testar={{ param.id }}" class="btn btn-sm btn-secondary">🧪 Testar</a> <button class="btn btn-sm btn-primary" onclick="salvarInline('{{ param.id }}')">💾 Salvar</button>
<a href="/parametros/delete/{{ param.id }}" class="btn btn-sm btn-danger">🗑️ Excluir</a> <a href="/parametros/delete/{{ param.id }}" class="btn btn-sm btn-danger" onclick="return confirm('Deseja excluir?')">🗑️ Excluir</a>
</div> </div>
</div> </div>
{% else %}<p style="color:gray;">Nenhuma fórmula cadastrada.</p>{% endfor %} <div class="mensagem-info" id="resultado-inline-{{ param.id }}" style="margin-top: 0.5rem; display:none;"></div>
</div>
{% else %}
<p style="color:gray;">Nenhuma fórmula cadastrada.</p>
{% endfor %}
</div> </div>
</div> </div>
<!-- ABA SELIC --> <!-- ABA SELIC -->
<div id="selic" class="tab-content" style="display:none;"> <div id="selic" class="tab-content">
<div class="formulario-box"> <div class="formulario-box">
<h3>📈 Gestão da SELIC</h3>
<p>Utilize o botão abaixo para importar os fatores SELIC automaticamente a partir da API do Banco Central.</p> <p>Utilize o botão abaixo para importar os fatores SELIC automaticamente a partir da API do Banco Central.</p>
<form method="post" action="/parametros/selic/importar"> <form method="post" action="/parametros/selic/importar" onsubmit="mostrarLoadingSelic()">
<div class="form-group"> <div class="form-group">
<label for="data_maxima">Data máxima para cálculo da SELIC:</label> <label for="data_maxima">Data máxima para cálculo da SELIC:</label>
<input type="date" id="data_maxima" name="data_maxima" value="{{ data_maxima or '' }}" /> <input type="date" id="data_maxima" name="data_maxima" value="{{ data_maxima or '' }}" />
</div> </div>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<button type="submit" class="btn btn-primary">⬇️ Atualizar Fatores SELIC</button> <button type="submit" class="btn btn-primary">⬇️ Atualizar Fatores SELIC</button>
</form> <button type="button" class="btn btn-secondary" onclick="mostrarFeedback('🔁 Atualização', 'Função de recarga futura')">🔄 Recarregar</button>
</div>
<div class="mensagem-info" style="margin-top:1rem;">Última data coletada da SELIC: <strong>{{ ultima_data_selic }}</strong></div> <div class="mensagem-info" style="margin-top:1rem;">Última data coletada da SELIC: <strong>{{ ultima_data_selic }}</strong></div>
</form>
<table class="selic-table"> <table class="selic-table">
<thead><tr><th>Competência</th><th>Fator</th></tr></thead> <thead><tr><th>Competência</th><th>Fator</th></tr></thead>
<tbody> <tbody>
{% for item in selic_dados %} {% for item in selic_dados %}
<tr><td>{{ item.competencia }}</td><td>{{ item.fator }}</td></tr> <tr>
<td>{{ "%02d"|format(item.mes) }}/{{ item.ano }}</td>
<td>{{ "%.4f"|format(item.percentual) }}</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<!-- ABA ALÍQUOTAS -->
<div id="aliquotas" class="tab-content">
<div class="formulario-box">
<form onsubmit="return salvarAliquota(this)">
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));">
<div class="form-group">
<label>UF:</label>
<select name="uf" required>
<option value="">Selecione o Estado</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>
</div>
<div class="form-group">
<label>Exercício:</label>
<input name="exercicio" type="number" required />
</div>
<div class="form-group">
<label>Alíquota ICMS (%):</label>
<input name="aliquota" type="number" step="0.0001" required />
</div>
</div>
<!-- Bloco com espaçamento e alinhamento central -->
<div class="grupo-botoes">
<!-- Botão salvar -->
<button class="btn btn-primary" type="submit">💾 Salvar Alíquota</button>
<!-- Importação e template -->
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem;">
<label for="arquivo_aliquotas" class="btn btn-secondary" style="cursor: pointer;">
📎 Importar CSV
<input type="file" name="arquivo_aliquotas" accept=".csv" onchange="enviarArquivoAliquotas(this)" style="display: none;" />
</label>
<a href="/parametros/aliquotas/template" class="btn btn-secondary">📥 Baixar Template CSV</a>
</div>
</div>
</form>
<table class="selic-table">
<thead><tr><th>UF</th><th>Exercício</th><th>Alíquota</th></tr></thead>
<tbody id="tabela-aliquotas"></tbody>
</table>
</div>
</div>
<!-- ESTILOS --> <!-- ESTILOS -->
<style> <style>
.tabs { display: flex; gap: 1rem; margin-bottom: 1rem; } /* Abas */
.tab { background: none; border: none; font-weight: bold; cursor: pointer; padding: 0.5rem 1rem; border-bottom: 2px solid transparent; } .tabs {
.tab.active { border-bottom: 2px solid #2563eb; color: #2563eb; } display: flex;
.tab-content { display: none; } justify-content: center;
.tab-content.active { display: block; } gap: 1rem;
margin-bottom: 1rem;
}
.tab {
background: none;
border: none;
font-weight: bold;
cursor: pointer;
padding: 0.5rem 1rem;
border-bottom: 2px solid transparent;
}
.tab.active {
border-bottom: 2px solid #2563eb;
color: #2563eb;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Formulário principal */
.formulario-box { .formulario-box {
background: #fff; background: #fff;
padding: 2rem; padding: 2rem;
@@ -112,95 +207,473 @@
margin: 0 auto; margin: 0 auto;
} }
.form-group { margin-bottom: 1rem; } .form-group {
.form-group label { display: block; font-weight: bold; margin-bottom: 0.5rem; } margin-bottom: 1rem;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 0.5rem;
}
.form-group input[type="text"], .form-group input[type="text"],
.form-group input[type="number"], .form-group input[type="number"],
.form-group input[type="date"], .form-group input[type="date"],
.form-group textarea { width: 100%; padding: 0.6rem; border-radius: 6px; border: 1px solid #ccc; } .form-group textarea {
width: 100%;
padding: 0.6rem;
border-radius: 6px;
border: 1px solid #ccc;
}
.form-group.check-group { display: flex; gap: 1rem; flex-wrap: wrap; } .form-group.check-group {
.check-group label { display: flex; align-items: center; gap: 0.5rem; } display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.check-group label {
display: flex;
align-items: center;
gap: 0.5rem;
}
.editor-box select { margin-bottom: 0.5rem; } /* Grade do formulário */
.editor-box textarea { font-family: monospace; } .grid {
.actions-inline { display: flex; gap: 1rem; align-items: center; margin-top: 0.5rem; } display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; } /* Botões */
.btn-primary { background-color: #2563eb; color: white; } .btn {
.btn-secondary { background-color: #6c757d; color: white; } padding: 0.5rem 1rem;
.btn-danger { background-color: #dc3545; color: white; } border: none;
.btn-sm { font-size: 0.85rem; padding: 0.3rem 0.6rem; } border-radius: 6px;
font-weight: 600;
cursor: pointer;
}
.btn-primary {
background-color: #2563eb;
color: white;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-sm {
font-size: 0.85rem;
padding: 0.3rem 0.6rem;
}
/* Cards de fórmulas salvas */
.card-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
align-items: start;
}
.param-card {
display: flex;
flex-direction: column;
justify-content: flex-start;
background: #f9f9f9;
padding: 1rem;
border-radius: 8px;
border-left: 4px solid #2563eb;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
min-height: 200px;
}
.param-card.ativo {
border-left-color: #198754;
background: #f6fff9;
}
.param-card.inativo {
border-left-color: #adb5bd;
background: #f9f9f9;
}
.param-card input.edit-nome {
font-weight: bold;
font-size: 1rem;
border: none;
background: transparent;
outline: none;
flex: 1;
width: 100%;
padding: 0.25rem 0;
}
.edit-formula {
width: 100%;
font-family: monospace;
border: 1px solid #ccc;
border-radius: 6px;
padding: 0.5rem;
margin: 0.5rem 0;
resize: vertical;
min-height: 60px;
}
.param-card .actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}
.badge-status {
font-size: 0.75rem;
font-weight: bold;
padding: 0.2rem 0.6rem;
border-radius: 12px;
color: white;
background: #198754;
}
.param-card.inativo .badge-status {
background: #adb5bd;
}
/* Badge de campos e operadores */
.campo-badges {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.5rem 0;
}
.badge-campo, .badge-operador {
background: #e0e7ff;
color: #1e3a8a;
padding: 0.3rem 0.6rem;
border-radius: 8px;
cursor: pointer;
font-family: monospace;
transition: background 0.2s ease;
}
.badge-campo:hover, .badge-operador:hover {
background: #c7d2fe;
}
/* Mensagens */
.mensagem-info { .mensagem-info {
background: #e0f7fa; background: #e0f7fa;
padding: 1rem; padding: 1rem;
border-left: 4px solid #2563eb; border-left: 4px solid #2563eb;
border-radius: 6px; border-radius: 6px;
color: #007b83; color: #007b83;
font-size: 0.85rem;
} }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } /* Tabela SELIC */
.selic-table {
.card-list { width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin-top: 1rem; margin-top: 1rem;
border-collapse: collapse;
} }
.param-card { .selic-table th,
background: #f9f9f9; .selic-table td {
padding: 1rem; padding: 0.6rem;
border-radius: 8px; border-bottom: 1px solid #ccc;
border-left: 4px solid #2563eb; }
.selic-table th {
text-align: left;
background: #f1f1f1;
} }
.param-card code { display: block; margin: 0.5rem 0; color: #333; }
.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.selic-table { width: 100%; margin-top: 1rem; border-collapse: collapse; } /* Popup de feedback */
.selic-table th, .selic-table td { padding: 0.6rem; border-bottom: 1px solid #ccc; } .feedback-popup {
.selic-table th { text-align: left; background: #f1f1f1; } position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
}
.feedback-content {
background-color: #fff;
padding: 2rem;
border-radius: 12px;
text-align: center;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
}
.feedback-content h3 {
margin-bottom: 1rem;
}
.feedback-content p {
margin: 0.25rem 0;
font-weight: 500;
}
.hidden {
display: none !important;
}
.form-group select {
width: 100%;
padding: 0.6rem;
border-radius: 6px;
border: 1px solid #ccc;
font-family: inherit;
}
.btn {
text-decoration: none;
}
.grupo-botoes {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 1.5rem;
gap: 1rem;
}
.btn {
padding: 0.5rem 1rem;
font-weight: 600;
font-size: 1rem;
border-radius: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
text-decoration: none !important;
}
</style> </style>
{% block scripts %}
<script> <script>
// 🟡 Alterna entre abas
function switchTab(tabId) { function switchTab(tabId) {
// Remove classe 'active' de todos os botões e abas
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
// Ativa a aba correspondente
document.getElementById(tabId).classList.add('active'); document.getElementById(tabId).classList.add('active');
event.target.classList.add('active');
// Ativa o botão correspondente
const button = document.querySelector(`.tab[onclick="switchTab('${tabId}')"]`);
if (button) button.classList.add('active');
} }
function inserirCampo(select) {
const campo = select.value; // ✅ Insere valores no editor de fórmulas
if (campo) { function inserirNoEditor(valor) {
const formula = document.getElementById("formula"); const formula = document.getElementById("formula");
const start = formula.selectionStart; const start = formula.selectionStart;
const end = formula.selectionEnd; const end = formula.selectionEnd;
formula.value = formula.value.slice(0, start) + campo + formula.value.slice(end); formula.value = formula.value.slice(0, start) + valor + formula.value.slice(end);
formula.focus(); formula.focus();
formula.setSelectionRange(start + campo.length, start + campo.length); formula.setSelectionRange(start + valor.length, start + valor.length);
select.selectedIndex = 0; }
// ✅ Feedback visual em popup
function mostrarFeedback(titulo, mensagem) {
document.getElementById("feedback-titulo").innerText = titulo;
document.getElementById("feedback-mensagem").innerText = mensagem;
document.getElementById("parametros-feedback").classList.remove("hidden");
}
function fecharFeedbackParametros() {
document.getElementById("parametros-feedback").classList.add("hidden");
}
// ✅ Testa fórmula principal (formulário superior)
function testarFormula() {
const nome = document.getElementById("nome").value.trim();
const formula = document.getElementById("formula").value.trim();
const output = document.getElementById("resultado-teste");
if (!nome || !formula) {
output.innerText = "❌ Preencha o nome e a fórmula para testar.";
output.style.display = "block";
return;
}
fetch("/parametros/testar", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nome, formula })
})
.then(res => res.json())
.then(data => {
output.style.display = "block";
if (data.success) {
output.innerText = `✅ Fórmula válida. Resultado: ${data.resultado}`;
} else {
output.innerText = `❌ Erro: ${data.error}`;
}
});
}
// ✅ Testa fórmula inline nos cards salvos
function testarFormulaInline(id) {
const nome = document.querySelector(`.edit-nome[data-id='${id}']`)?.value;
const formula = document.querySelector(`.edit-formula[data-id='${id}']`)?.value;
const output = document.getElementById(`resultado-inline-${id}`);
if (!formula) {
output.innerText = "❌ Fórmula não preenchida.";
output.style.display = 'block';
return;
}
fetch("/parametros/testar", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nome, formula })
})
.then(res => res.json())
.then(data => {
output.style.display = 'block';
if (data.success) {
output.innerText = `✅ Fórmula válida. Resultado: ${data.resultado}`;
} else {
output.innerText = `❌ Erro: ${data.error}`;
}
});
}
// ✅ Salva edição inline nos cards
async function salvarInline(id) {
const inputNome = document.querySelector(`.edit-nome[data-id='${id}']`);
const textareaFormula = document.querySelector(`.edit-formula[data-id='${id}']`);
const nome = inputNome.value.trim();
const formula = textareaFormula.value.trim();
if (!nome || !formula) {
alert("Preencha todos os campos.");
return;
}
const response = await fetch(`/parametros/editar/${id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nome, formula })
});
if (response.ok) {
mostrarFeedback("✅ Atualizado", "Parâmetro salvo com sucesso.");
} else {
mostrarFeedback("❌ Erro", "Erro ao salvar.");
} }
} }
function testarFormula() { // ✅ Carrega tabela de alíquotas
const formula = document.getElementById("formula").value; async function carregarAliquotas() {
const vars = { const res = await fetch("/parametros/aliquotas");
pis_base: 84.38, const dados = await res.json();
cofins_base: 84.38, const tbody = document.getElementById("tabela-aliquotas");
icms_valor: 24.47, tbody.innerHTML = dados.map(a => `
pis_aliq: 0.012872, <tr><td>${a.uf}</td><td>${a.exercicio}</td><td>${a.aliquota.toFixed(4)}%</td></tr>
cofins_aliq: 0.059287 `).join('');
};
try {
const resultado = eval(formula.replace(/\b(\w+)\b/g, match => vars.hasOwnProperty(match) ? vars[match] : match));
document.getElementById("resultado-teste").innerText = "Resultado: R$ " + resultado.toFixed(5);
document.getElementById("resultado-teste").style.display = "block";
} catch (e) {
document.getElementById("resultado-teste").innerText = "Erro: " + e.message;
document.getElementById("resultado-teste").style.display = "block";
} }
// ✅ Eventos após carregar DOM
document.addEventListener('DOMContentLoaded', () => {
carregarAliquotas();
// Ativar/desativar checkbox
document.querySelectorAll('.toggle-ativo').forEach(input => {
input.addEventListener('change', async function () {
const id = this.dataset.id;
const ativo = this.checked;
const response = await fetch(`/parametros/ativar/${id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ativo })
});
if (response.ok) {
location.reload();
} else {
alert('Erro ao atualizar status.');
} }
});
});
// Botões de teste inline
document.querySelectorAll('.btn-testar').forEach(button => {
button.addEventListener('click', function () {
const id = this.dataset.id;
testarFormulaInline(id);
});
});
});
function mostrarLoadingSelic() {
document.getElementById("selic-loading").classList.remove("hidden");
}
window.addEventListener("DOMContentLoaded", () => {
const abaUrl = new URLSearchParams(window.location.search).get("aba");
if (abaUrl === "selic") {
document.querySelector(".tab.active")?.classList.remove("active");
document.querySelector(".tab-content.active")?.classList.remove("active");
document.querySelector(".tab:nth-child(2)").classList.add("active"); // Ativa o botão da aba
document.getElementById("selic").classList.add("active"); // Ativa o conteúdo da aba
}
});
function enviarArquivoAliquotas(input) {
const file = input.files[0];
if (!file) return;
const formData = new FormData();
formData.append("arquivo", file);
fetch("/parametros/aliquotas/importar", {
method: "POST",
body: formData
})
.then(res => res.json())
.then(data => {
if (data.success) {
mostrarFeedback("✅ Importado", `${data.qtd} alíquotas foram importadas.`);
carregarAliquotas();
} else {
mostrarFeedback("❌ Erro", data.error || "Falha na importação.");
}
});
}
</script> </script>
<!-- Feedback estilo popup -->
<div id="parametros-feedback" class="feedback-popup hidden">
<div class="feedback-content">
<h3 id="feedback-titulo">✅ Ação Concluída</h3>
<p id="feedback-mensagem">Parâmetro salvo com sucesso.</p>
<button onclick="fecharFeedbackParametros()" class="btn btn-primary" style="margin-top: 1rem;">OK</button>
</div>
</div>
<div id="selic-loading" class="feedback-popup hidden">
<div class="feedback-content">
<h3>⏳ Atualizando SELIC</h3>
<p>Aguarde enquanto os fatores SELIC estão sendo carregados...</p>
</div>
</div>
{% endblock %}
{% endblock %} {% endblock %}

View File

@@ -16,6 +16,12 @@
<div class="buttons"> <div class="buttons">
<button class="btn btn-primary" onclick="processar(this)">Processar Faturas</button> <button class="btn btn-primary" onclick="processar(this)">Processar Faturas</button>
<button class="btn btn-primary pulse" onclick="limpar()" style="font-weight: bold;">🔁 Novo Processo</button> <button class="btn btn-primary pulse" onclick="limpar()" style="font-weight: bold;">🔁 Novo Processo</button>
{% if status_resultados|selectattr("status", "equalto", "Erro")|list %}
<div style="margin-top: 2rem;">
<a class="btn btn-danger" href="/erros/download">⬇️ Baixar Faturas com Erro (.zip)</a>
<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="baixarPlanilha()">📅 Abrir Planilha</button>
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button> <button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button>
{% if app_env != "producao" %} {% if app_env != "producao" %}
@@ -28,7 +34,7 @@
</div> </div>
<div id="tabela-wrapper" class="tabela-wrapper"></div> <div id="tabela-wrapper" class="tabela-wrapper"></div>
</div> </div>
ar
<script> <script>
let arquivos = []; let arquivos = [];
let statusInterval = null; let statusInterval = null;
@@ -77,7 +83,10 @@ function renderTable(statusList = []) {
grupo === 'Duplicado' ? '📄' : grupo === 'Duplicado' ? '📄' :
'⌛'} ${file.status} '⌛'} ${file.status}
</td> </td>
<td>${file.tempo || '---'}</td> <td>
${file.mensagem ? `<div class="status-msg">${file.mensagem}</div>` : ""}
${file.tempo || '---'}
</td>
</tr> </tr>
`).join(''); `).join('');
@@ -517,6 +526,12 @@ function fecharFeedback() {
100% { transform: scale(1); } 100% { transform: scale(1); }
} }
.status-msg {
color: #dc3545;
font-size: 0.8rem;
margin-top: 0.25rem;
white-space: pre-wrap;
}
</style> </style>

View File

@@ -63,3 +63,9 @@ async def adicionar_fatura(dados, caminho_pdf):
logger.error(f"Erro ao adicionar fatura no banco: {e}") logger.error(f"Erro ao adicionar fatura no banco: {e}")
await session.rollback() await session.rollback()
raise raise
def avaliar_formula(formula: str, contexto: dict):
try:
return eval(formula, {}, contexto)
except Exception as e:
return None