Atualização: template Excel de alíquotas e layout da aba
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
140
app/main.py
140
app/main.py
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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"}
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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 %}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user