Cadastro da alíquota do ICMS correta. Inclusão da nova alíquota e comparação em todos os relatórios.
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
157
app/main.py
157
app/main.py
@@ -29,6 +29,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app.database import get_session
|
from app.database import get_session
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from sqlalchemy import select as sqla_select
|
from sqlalchemy import select as sqla_select
|
||||||
|
from app.models import AliquotaUF
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
@@ -40,29 +42,25 @@ UPLOAD_DIR = os.path.join("app", "uploads", "temp")
|
|||||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||||
|
|
||||||
def _parse_referencia(ref: str):
|
def _parse_referencia(ref: str):
|
||||||
"""Aceita 'JAN/2024', 'JAN/24', '01/2024', '01/24', '202401'. Retorna (ano, mes)."""
|
"""Aceita 'JAN/2024', '01/2024', '202401' etc. Retorna (ano, mes)."""
|
||||||
meses = {'JAN':1,'FEV':2,'MAR':3,'ABR':4,'MAI':5,'JUN':6,'JUL':7,'AGO':8,'SET':9,'OUT':10,'NOV':11,'DEZ':12}
|
meses = {'JAN':1,'FEV':2,'MAR':3,'ABR':4,'MAI':5,'JUN':6,'JUL':7,'AGO':8,'SET':9,'OUT':10,'NOV':11,'DEZ':12}
|
||||||
ref = (ref or "").strip().upper()
|
ref = (ref or "").strip().upper()
|
||||||
|
|
||||||
if "/" in ref:
|
if "/" in ref:
|
||||||
a, b = [p.strip() for p in ref.split("/", 1)]
|
a, b = [p.strip() for p in ref.split("/", 1)]
|
||||||
# mês pode vir 'JAN' ou '01'
|
|
||||||
mes = meses.get(a, None)
|
mes = meses.get(a, None)
|
||||||
if mes is None:
|
if mes is None:
|
||||||
mes = int(re.sub(r"\D", "", a) or 1)
|
mes = int(re.sub(r"\D", "", a) or 1)
|
||||||
ano = int(re.sub(r"\D", "", b) or 0)
|
ano = int(re.sub(r"\D", "", b) or 0)
|
||||||
# ano 2 dígitos -> 2000+
|
|
||||||
if ano < 100:
|
if ano < 100:
|
||||||
ano += 2000
|
ano += 2000
|
||||||
else:
|
else:
|
||||||
# '202401' ou '2024-01'
|
|
||||||
num = re.sub(r"\D", "", ref)
|
num = re.sub(r"\D", "", ref)
|
||||||
if len(num) >= 6:
|
if len(num) >= 6:
|
||||||
ano, mes = int(num[:4]), int(num[4:6])
|
ano, mes = int(num[:4]), int(num[4:6])
|
||||||
elif len(num) == 4: # '2024'
|
elif len(num) == 4:
|
||||||
ano, mes = int(num), 1
|
ano, mes = int(num), 1
|
||||||
else:
|
else:
|
||||||
ano, mes = date.today().year, 1
|
ano, mes = 0, 0
|
||||||
return ano, mes
|
return ano, mes
|
||||||
|
|
||||||
async def _carregar_selic_map(session):
|
async def _carregar_selic_map(session):
|
||||||
@@ -373,46 +371,59 @@ async def clear_all():
|
|||||||
|
|
||||||
@app.get("/export-excel")
|
@app.get("/export-excel")
|
||||||
async def export_excel(
|
async def export_excel(
|
||||||
tipo: str = Query("geral", regex="^(geral|exclusao_icms|aliquota_icms)$"),
|
tipo: str = Query("geral", pattern="^(geral|exclusao_icms|aliquota_icms)$"),
|
||||||
cliente: str | None = Query(None)
|
cliente: str | None = Query(None)
|
||||||
):
|
):
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
# 1) Carregar faturas (com filtro por cliente, se houver)
|
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
|
# 1) Faturas
|
||||||
stmt = select(Fatura)
|
stmt = select(Fatura)
|
||||||
if cliente:
|
if cliente:
|
||||||
# filtra por cliente_id (UUID em string)
|
|
||||||
stmt = stmt.where(Fatura.cliente_id == cliente)
|
stmt = stmt.where(Fatura.cliente_id == cliente)
|
||||||
faturas_result = await session.execute(stmt)
|
faturas = (await session.execute(stmt)).scalars().all()
|
||||||
faturas = faturas_result.scalars().all()
|
|
||||||
|
# 2) Mapa de alíquotas cadastradas (UF/ano)
|
||||||
|
aliq_rows = (await session.execute(select(AliquotaUF))).scalars().all()
|
||||||
|
aliq_map = {(r.uf.upper(), int(r.exercicio)): float(r.aliq_icms) for r in aliq_rows}
|
||||||
|
|
||||||
# 2) Montar dados conforme 'tipo'
|
|
||||||
dados = []
|
dados = []
|
||||||
|
|
||||||
if tipo == "aliquota_icms":
|
if tipo == "aliquota_icms":
|
||||||
# Campos: (Cliente, UC, Referência, Valor Total, ICMS (%), ICMS (R$),
|
|
||||||
# Base ICMS (R$), Consumo (kWh), Tarifa, Nota Fiscal)
|
|
||||||
for f in faturas:
|
for f in faturas:
|
||||||
|
uf = (f.estado or "").strip().upper()
|
||||||
|
ano, _ = _parse_referencia(f.referencia or "")
|
||||||
|
aliq_nf = float(f.icms_aliq or 0.0)
|
||||||
|
aliq_cad = aliq_map.get((uf, ano))
|
||||||
|
diff_pp = (aliq_nf - aliq_cad) if aliq_cad is not None else None
|
||||||
|
confere = (abs(diff_pp) < 1e-6) if diff_pp is not None else None
|
||||||
|
|
||||||
dados.append({
|
dados.append({
|
||||||
"Cliente": f.nome,
|
"Cliente": f.nome,
|
||||||
"UC": f.unidade_consumidora,
|
"UF (fatura)": uf,
|
||||||
|
"Exercício (ref)": ano,
|
||||||
"Referência": f.referencia,
|
"Referência": f.referencia,
|
||||||
"Valor Total": f.valor_total,
|
|
||||||
"ICMS (%)": f.icms_aliq,
|
|
||||||
"ICMS (R$)": f.icms_valor,
|
|
||||||
"Base ICMS (R$)": f.icms_base,
|
|
||||||
"Consumo (kWh)": f.consumo,
|
|
||||||
"Tarifa": f.tarifa,
|
|
||||||
"Nota Fiscal": f.nota_fiscal,
|
"Nota Fiscal": f.nota_fiscal,
|
||||||
|
"ICMS (%) NF": aliq_nf,
|
||||||
|
|
||||||
|
# novas colunas padronizadas
|
||||||
|
"ICMS (%) (UF/Ref)": aliq_cad,
|
||||||
|
"Dif. ICMS (pp)": diff_pp,
|
||||||
|
"ICMS confere?": "SIM" if confere else ("N/D" if confere is None else "NÃO"),
|
||||||
|
|
||||||
|
"Valor Total": f.valor_total,
|
||||||
|
"Distribuidora": f.distribuidora,
|
||||||
|
"Data Processamento": f.data_processamento,
|
||||||
})
|
})
|
||||||
filename = "relatorio_aliquota_icms.xlsx"
|
filename = "relatorio_aliquota_icms.xlsx"
|
||||||
|
|
||||||
elif tipo == "exclusao_icms":
|
elif tipo == "exclusao_icms":
|
||||||
# Campos: (Cliente, UC, Referência, Valor Total, PIS (%), ICMS (%), COFINS (%),
|
|
||||||
# PIS (R$), ICMS (R$), COFINS (R$), Base PIS (R$), Base ICMS (R$),
|
|
||||||
# Base COFINS (R$), Consumo (kWh), Tarifa, Nota Fiscal)
|
|
||||||
for f in faturas:
|
for f in faturas:
|
||||||
|
uf = (f.estado or "").strip().upper()
|
||||||
|
ano, _ = _parse_referencia(f.referencia or "")
|
||||||
|
aliq_nf = float(f.icms_aliq or 0.0)
|
||||||
|
aliq_cad = aliq_map.get((uf, ano))
|
||||||
|
diff_pp = (aliq_nf - aliq_cad) if aliq_cad is not None else None
|
||||||
|
confere = (abs(diff_pp) < 1e-6) if diff_pp is not None else None
|
||||||
|
|
||||||
dados.append({
|
dados.append({
|
||||||
"Cliente": f.nome,
|
"Cliente": f.nome,
|
||||||
"UC": f.unidade_consumidora,
|
"UC": f.unidade_consumidora,
|
||||||
@@ -427,17 +438,27 @@ async def export_excel(
|
|||||||
"Base PIS (R$)": f.pis_base,
|
"Base PIS (R$)": f.pis_base,
|
||||||
"Base ICMS (R$)": f.icms_base,
|
"Base ICMS (R$)": f.icms_base,
|
||||||
"Base COFINS (R$)": f.cofins_base,
|
"Base COFINS (R$)": f.cofins_base,
|
||||||
|
|
||||||
|
# novas colunas
|
||||||
|
"ICMS (%) (UF/Ref)": aliq_cad,
|
||||||
|
"Dif. ICMS (pp)": diff_pp,
|
||||||
|
"ICMS confere?": "SIM" if confere else ("N/D" if confere is None else "NÃO"),
|
||||||
|
|
||||||
"Consumo (kWh)": f.consumo,
|
"Consumo (kWh)": f.consumo,
|
||||||
"Tarifa": f.tarifa,
|
"Tarifa": f.tarifa,
|
||||||
"Nota Fiscal": f.nota_fiscal,
|
"Nota Fiscal": f.nota_fiscal,
|
||||||
})
|
})
|
||||||
filename = "relatorio_exclusao_icms.xlsx"
|
filename = "relatorio_exclusao_icms.xlsx"
|
||||||
|
|
||||||
else: # "geral" (mantém seu relatório atual — sem fórmulas SELIC)
|
else: # geral
|
||||||
# Se quiser manter exatamente o que já tinha com SELIC e fórmulas,
|
|
||||||
# você pode copiar sua lógica anterior aqui. Abaixo deixo um "geral"
|
|
||||||
# simplificado com as colunas principais.
|
|
||||||
for f in faturas:
|
for f in faturas:
|
||||||
|
uf = (f.estado or "").strip().upper()
|
||||||
|
ano, _ = _parse_referencia(f.referencia or "")
|
||||||
|
aliq_nf = float(f.icms_aliq or 0.0)
|
||||||
|
aliq_cad = aliq_map.get((uf, ano))
|
||||||
|
diff_pp = (aliq_nf - aliq_cad) if aliq_cad is not None else None
|
||||||
|
confere = (abs(diff_pp) < 1e-6) if diff_pp is not None else None
|
||||||
|
|
||||||
dados.append({
|
dados.append({
|
||||||
"Cliente": f.nome,
|
"Cliente": f.nome,
|
||||||
"UC": f.unidade_consumidora,
|
"UC": f.unidade_consumidora,
|
||||||
@@ -446,6 +467,12 @@ async def export_excel(
|
|||||||
"Valor Total": f.valor_total,
|
"Valor Total": f.valor_total,
|
||||||
"ICMS (%)": f.icms_aliq,
|
"ICMS (%)": f.icms_aliq,
|
||||||
"ICMS (R$)": f.icms_valor,
|
"ICMS (R$)": f.icms_valor,
|
||||||
|
|
||||||
|
# novas colunas
|
||||||
|
"ICMS (%) (UF/Ref)": aliq_cad,
|
||||||
|
"Dif. ICMS (pp)": diff_pp,
|
||||||
|
"ICMS confere?": "SIM" if confere else ("N/D" if confere is None else "NÃO"),
|
||||||
|
|
||||||
"Base ICMS (R$)": f.icms_base,
|
"Base ICMS (R$)": f.icms_base,
|
||||||
"PIS (%)": f.pis_aliq,
|
"PIS (%)": f.pis_aliq,
|
||||||
"PIS (R$)": f.pis_valor,
|
"PIS (R$)": f.pis_valor,
|
||||||
@@ -460,15 +487,13 @@ async def export_excel(
|
|||||||
})
|
})
|
||||||
filename = "relatorio_geral.xlsx"
|
filename = "relatorio_geral.xlsx"
|
||||||
|
|
||||||
# 3) Gerar excel em memória
|
# 3) Excel em memória
|
||||||
from io import BytesIO
|
|
||||||
output = BytesIO()
|
output = BytesIO()
|
||||||
df = pd.DataFrame(dados)
|
df = pd.DataFrame(dados)
|
||||||
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
||||||
df.to_excel(writer, index=False, sheet_name="Relatório")
|
df.to_excel(writer, index=False, sheet_name="Relatório")
|
||||||
output.seek(0)
|
output.seek(0)
|
||||||
|
|
||||||
# 4) Responder
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
output,
|
output,
|
||||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
@@ -566,10 +591,12 @@ async def api_relatorios(
|
|||||||
if cliente:
|
if cliente:
|
||||||
params["cliente"] = cliente
|
params["cliente"] = cliente
|
||||||
|
|
||||||
|
# ❗ Inclua 'estado' no SELECT
|
||||||
sql = text(f"""
|
sql = text(f"""
|
||||||
SELECT id, nome, unidade_consumidora, referencia, nota_fiscal,
|
SELECT id, nome, unidade_consumidora, referencia, nota_fiscal,
|
||||||
valor_total, icms_aliq, icms_valor, pis_aliq, pis_valor,
|
valor_total, icms_aliq, icms_valor, pis_aliq, pis_valor,
|
||||||
cofins_aliq, cofins_valor, distribuidora, data_processamento
|
cofins_aliq, cofins_valor, distribuidora, data_processamento,
|
||||||
|
estado
|
||||||
FROM faturas.faturas
|
FROM faturas.faturas
|
||||||
{where}
|
{where}
|
||||||
ORDER BY data_processamento DESC
|
ORDER BY data_processamento DESC
|
||||||
@@ -580,21 +607,47 @@ async def api_relatorios(
|
|||||||
rows = (await db.execute(sql, params)).mappings().all()
|
rows = (await db.execute(sql, params)).mappings().all()
|
||||||
total = (await db.execute(count_sql, params)).scalar_one()
|
total = (await db.execute(count_sql, params)).scalar_one()
|
||||||
|
|
||||||
items = [{
|
# 🔹 Carrega mapa de alíquotas UF/ano
|
||||||
"id": str(r["id"]),
|
aliq_rows = (await db.execute(select(AliquotaUF))).scalars().all()
|
||||||
"nome": r["nome"],
|
aliq_map = {(r.uf.upper(), int(r.exercicio)): float(r.aliq_icms) for r in aliq_rows}
|
||||||
"unidade_consumidora": r["unidade_consumidora"],
|
|
||||||
"referencia": r["referencia"],
|
|
||||||
"nota_fiscal": r["nota_fiscal"],
|
|
||||||
"valor_total": float(r["valor_total"]) if r["valor_total"] is not None else None,
|
|
||||||
"icms_aliq": r["icms_aliq"],
|
|
||||||
"icms_valor": r["icms_valor"],
|
|
||||||
"pis_aliq": r["pis_aliq"],
|
|
||||||
"pis_valor": r["pis_valor"],
|
|
||||||
"cofins_aliq": r["cofins_aliq"],
|
|
||||||
"cofins_valor": r["cofins_valor"],
|
|
||||||
"distribuidora": r["distribuidora"],
|
|
||||||
"data_processamento": r["data_processamento"].isoformat() if r["data_processamento"] else None,
|
|
||||||
} for r in rows]
|
|
||||||
|
|
||||||
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
items = []
|
||||||
|
for r in rows:
|
||||||
|
uf = (r["estado"] or "").strip().upper()
|
||||||
|
ano, _mes = _parse_referencia(r["referencia"] or "")
|
||||||
|
aliq_nf = float(r["icms_aliq"] or 0.0)
|
||||||
|
aliq_cad = aliq_map.get((uf, ano))
|
||||||
|
diff_pp = (aliq_nf - aliq_cad) if aliq_cad is not None else None
|
||||||
|
ok = (abs(diff_pp) < 1e-6) if diff_pp is not None else None
|
||||||
|
|
||||||
|
items.append({
|
||||||
|
"id": str(r["id"]),
|
||||||
|
"nome": r["nome"],
|
||||||
|
"unidade_consumidora": r["unidade_consumidora"],
|
||||||
|
"referencia": r["referencia"],
|
||||||
|
"nota_fiscal": r["nota_fiscal"],
|
||||||
|
"valor_total": float(r["valor_total"]) if r["valor_total"] is not None else None,
|
||||||
|
"icms_aliq": aliq_nf,
|
||||||
|
"icms_valor": r["icms_valor"],
|
||||||
|
"pis_aliq": r["pis_aliq"],
|
||||||
|
"pis_valor": r["pis_valor"],
|
||||||
|
"cofins_aliq": r["cofins_aliq"],
|
||||||
|
"cofins_valor": r["cofins_valor"],
|
||||||
|
"distribuidora": r["distribuidora"],
|
||||||
|
"data_processamento": r["data_processamento"].isoformat() if r["data_processamento"] else None,
|
||||||
|
# novos
|
||||||
|
"estado": uf,
|
||||||
|
"exercicio": ano,
|
||||||
|
"aliq_cadastral": aliq_cad,
|
||||||
|
"aliq_diff_pp": round(diff_pp, 4) if diff_pp is not None else None,
|
||||||
|
"aliq_ok": ok,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
||||||
|
|
||||||
|
async def _carregar_aliquota_map(session):
|
||||||
|
rows = (await session.execute(
|
||||||
|
text("SELECT uf, exercicio, aliq_icms FROM faturas.aliquotas_uf")
|
||||||
|
)).mappings().all()
|
||||||
|
# (UF, ANO) -> float
|
||||||
|
return {(r["uf"].upper(), int(r["exercicio"])): float(r["aliq_icms"]) for r in rows}
|
||||||
@@ -72,7 +72,7 @@ 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(Integer)
|
||||||
aliq_icms = Column(Numeric(6, 4))
|
aliq_icms = Column(Numeric(6, 4))
|
||||||
|
|
||||||
class SelicMensal(Base):
|
class SelicMensal(Base):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# parametros.py
|
# parametros.py
|
||||||
from fastapi import APIRouter, Request, Depends, Form
|
from fastapi import APIRouter, Request, Depends, Form, UploadFile, File
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from 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
|
||||||
@@ -9,7 +9,7 @@ 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 fastapi.responses import RedirectResponse, JSONResponse
|
||||||
from app.models import Fatura
|
from app.models import Fatura
|
||||||
from fastapi import Body
|
from fastapi import Body
|
||||||
from app.database import engine
|
from app.database import engine
|
||||||
@@ -21,6 +21,8 @@ import csv
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from sqlalchemy import select
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -100,6 +102,28 @@ async def editar_parametro(param_id: int, request: Request):
|
|||||||
return {"success": True}
|
return {"success": True}
|
||||||
return {"success": False, "error": "Não encontrado"}
|
return {"success": False, "error": "Não encontrado"}
|
||||||
|
|
||||||
|
@router.post("/parametros/ativar/{param_id}")
|
||||||
|
async def ativar_parametro(param_id: int, request: Request):
|
||||||
|
data = await request.json()
|
||||||
|
ativo = bool(data.get("ativo", True))
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
param = await session.get(ParametrosFormula, param_id)
|
||||||
|
if not param:
|
||||||
|
return JSONResponse(status_code=404, content={"error": "Parâmetro não encontrado"})
|
||||||
|
param.ativo = ativo
|
||||||
|
await session.commit()
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
@router.get("/parametros/delete/{param_id}")
|
||||||
|
async def deletar_parametro(param_id: int):
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
param = await session.get(ParametrosFormula, param_id)
|
||||||
|
if not param:
|
||||||
|
return RedirectResponse("/parametros?erro=1&msg=Parâmetro não encontrado", status_code=303)
|
||||||
|
await session.delete(param)
|
||||||
|
await session.commit()
|
||||||
|
return RedirectResponse("/parametros?ok=1&msg=Parâmetro removido", status_code=303)
|
||||||
|
|
||||||
@router.post("/parametros/testar")
|
@router.post("/parametros/testar")
|
||||||
async def testar_formula(db: AsyncSession = Depends(get_session), data: dict = Body(...)):
|
async def testar_formula(db: AsyncSession = Depends(get_session), data: dict = Body(...)):
|
||||||
formula = data.get("formula")
|
formula = data.get("formula")
|
||||||
@@ -117,10 +141,18 @@ async def testar_formula(db: AsyncSession = Depends(get_session), data: dict = B
|
|||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema])
|
@router.get("/parametros/aliquotas")
|
||||||
async def listar_aliquotas(db: AsyncSession = Depends(get_session)):
|
async def listar_aliquotas(uf: str | None = None, db: AsyncSession = Depends(get_session)):
|
||||||
result = await db.execute(select(AliquotaUF).order_by(AliquotaUF.uf, AliquotaUF.exercicio))
|
stmt = select(AliquotaUF).order_by(AliquotaUF.uf, AliquotaUF.exercicio.desc())
|
||||||
return result.scalars().all()
|
if uf:
|
||||||
|
stmt = stmt.where(AliquotaUF.uf == uf)
|
||||||
|
|
||||||
|
rows = (await db.execute(stmt)).scalars().all()
|
||||||
|
return [
|
||||||
|
{"uf": r.uf, "exercicio": int(r.exercicio), "aliquota": float(r.aliq_icms)}
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/parametros/aliquotas")
|
@router.post("/parametros/aliquotas")
|
||||||
async def adicionar_aliquota(aliq: AliquotaUFSchema, db: AsyncSession = Depends(get_session)):
|
async def adicionar_aliquota(aliq: AliquotaUFSchema, db: AsyncSession = Depends(get_session)):
|
||||||
@@ -227,5 +259,88 @@ def baixar_template_excel():
|
|||||||
headers={"Content-Disposition": "attachment; filename=template_aliquotas.xlsx"}
|
headers={"Content-Disposition": "attachment; filename=template_aliquotas.xlsx"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.post("/parametros/aliquotas/salvar")
|
||||||
|
async def salvar_aliquota(payload: dict, db: AsyncSession = Depends(get_session)):
|
||||||
|
uf = (payload.get("uf") or "").strip().upper()
|
||||||
|
exercicio = int(payload.get("exercicio") or 0)
|
||||||
|
aliquota = Decimal(str(payload.get("aliquota") or "0"))
|
||||||
|
|
||||||
|
orig_uf = (payload.get("original_uf") or "").strip().upper() or uf
|
||||||
|
orig_ex = int(payload.get("original_exercicio") or 0) or exercicio
|
||||||
|
|
||||||
|
if not uf or not exercicio or aliquota <= 0:
|
||||||
|
return JSONResponse(status_code=400, content={"error": "UF, exercício e alíquota são obrigatórios."})
|
||||||
|
|
||||||
|
# busca pelo registro original (antes da edição)
|
||||||
|
stmt = select(AliquotaUF).where(
|
||||||
|
AliquotaUF.uf == orig_uf,
|
||||||
|
AliquotaUF.exercicio == orig_ex
|
||||||
|
)
|
||||||
|
existente = (await db.execute(stmt)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if existente:
|
||||||
|
# atualiza (inclusive a chave, se mudou)
|
||||||
|
existente.uf = uf
|
||||||
|
existente.exercicio = exercicio
|
||||||
|
existente.aliq_icms = aliquota
|
||||||
|
else:
|
||||||
|
# não existia o original -> upsert padrão
|
||||||
|
db.add(AliquotaUF(uf=uf, exercicio=exercicio, aliq_icms=aliquota))
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
@router.post("/parametros/aliquotas/importar")
|
||||||
|
async def importar_aliquotas_csv(arquivo: UploadFile = File(...), db: AsyncSession = Depends(get_session)):
|
||||||
|
content = await arquivo.read()
|
||||||
|
text = content.decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
# tenta ; depois ,
|
||||||
|
sniffer = csv.Sniffer()
|
||||||
|
dialect = sniffer.sniff(text.splitlines()[0] if text else "uf;exercicio;aliquota")
|
||||||
|
reader = csv.DictReader(io.StringIO(text), dialect=dialect)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for row in reader:
|
||||||
|
uf = (row.get("uf") or row.get("UF") or "").strip().upper()
|
||||||
|
exercicio_str = (row.get("exercicio") or row.get("ano") or "").strip()
|
||||||
|
try:
|
||||||
|
exercicio = int(exercicio_str)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
aliquota_str = (row.get("aliquota") or row.get("aliq_icms") or "").replace(",", ".").strip()
|
||||||
|
|
||||||
|
if not uf or not exercicio or not aliquota_str:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
aliquota = Decimal(aliquota_str)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
stmt = select(AliquotaUF).where(AliquotaUF.uf == uf, AliquotaUF.exercicio == exercicio)
|
||||||
|
existente = (await db.execute(stmt)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if existente:
|
||||||
|
existente.aliq_icms = aliquota
|
||||||
|
else:
|
||||||
|
db.add(AliquotaUF(uf=uf, exercicio=exercicio, aliq_icms=aliquota))
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
return {"success": True, "qtd": count}
|
||||||
|
|
||||||
|
@router.delete("/parametros/aliquotas/{uf}/{exercicio}")
|
||||||
|
async def excluir_aliquota(uf: str, exercicio: int, db: AsyncSession = Depends(get_session)):
|
||||||
|
stmt = select(AliquotaUF).where(
|
||||||
|
AliquotaUF.uf == uf.upper(),
|
||||||
|
AliquotaUF.exercicio == exercicio
|
||||||
|
)
|
||||||
|
row = (await db.execute(stmt)).scalar_one_or_none()
|
||||||
|
if not row:
|
||||||
|
return JSONResponse(status_code=404, content={"error": "Registro não encontrado."})
|
||||||
|
|
||||||
|
await db.delete(row)
|
||||||
|
await db.commit()
|
||||||
|
return {"success": True}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</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>
|
||||||
<input type="number" step="0.01" name="aliquota_icms" id="aliquota_icms" value="{{ parametros.aliquota_icms or '' }}" />
|
<input type="text" id="aliquota" name="aliquota" inputmode="decimal" pattern="[0-9]+([,][0-9]+)?" placeholder="Ex: 20,7487">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,6 +62,7 @@
|
|||||||
<hr style="margin-top: 2rem; margin-bottom: 1rem;">
|
<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">
|
||||||
|
{% if
|
||||||
{% for param in lista_parametros %}
|
{% for param in lista_parametros %}
|
||||||
<div class="param-card {{ 'ativo' if param.ativo else 'inativo' }}" id="card-{{ param.id }}">
|
<div class="param-card {{ 'ativo' if param.ativo else 'inativo' }}" id="card-{{ param.id }}">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
@@ -124,7 +125,7 @@
|
|||||||
<!-- ABA ALÍQUOTAS -->
|
<!-- ABA ALÍQUOTAS -->
|
||||||
<div id="aliquotas" class="tab-content">
|
<div id="aliquotas" class="tab-content">
|
||||||
<div class="formulario-box">
|
<div class="formulario-box">
|
||||||
<form onsubmit="return salvarAliquota(this)">
|
<form onsubmit="return salvarAliquota(this, event)">
|
||||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));">
|
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>UF:</label>
|
<label>UF:</label>
|
||||||
@@ -141,7 +142,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Alíquota ICMS (%):</label>
|
<label>Alíquota ICMS (%):</label>
|
||||||
<input name="aliquota" type="number" step="0.0001" required />
|
<input id="aliquota-uf" name="aliquota"
|
||||||
|
type="text" inputmode="decimal"
|
||||||
|
pattern="[0-9]+([,][0-9]+)?"
|
||||||
|
placeholder="Ex: 20,7487" required />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Bloco com espaçamento e alinhamento central -->
|
<!-- Bloco com espaçamento e alinhamento central -->
|
||||||
@@ -160,12 +164,35 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" id="orig-uf" name="original_uf">
|
||||||
|
<input type="hidden" id="orig-exercicio" name="original_exercicio">
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="selic-table">
|
<!-- Filtro de UF para a tabela -->
|
||||||
<thead><tr><th>UF</th><th>Exercício</th><th>Alíquota</th></tr></thead>
|
<div style="display:flex; align-items:center; gap:12px; margin:14px 0;">
|
||||||
<tbody id="tabela-aliquotas"></tbody>
|
<label for="filtro-uf" style="font-weight:600;">Filtrar por UF:</label>
|
||||||
</table>
|
<select id="filtro-uf" style="min-width:220px; padding:.5rem .75rem; border:1px solid #ddd; border-radius:8px;">
|
||||||
|
<option value="">Todas</option>
|
||||||
|
{% for uf in ['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO'] %}
|
||||||
|
<option value="{{ uf }}">{{ uf }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<span id="total-aliquotas" class="muted"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="selic-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>UF</th>
|
||||||
|
<th>Exercício</th>
|
||||||
|
<th>Alíquota</th>
|
||||||
|
<th style="width:140px;">Ações</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tabela-aliquotas"></tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -581,16 +608,39 @@
|
|||||||
|
|
||||||
// ✅ Carrega tabela de alíquotas
|
// ✅ Carrega tabela de alíquotas
|
||||||
async function carregarAliquotas() {
|
async function carregarAliquotas() {
|
||||||
const res = await fetch("/parametros/aliquotas");
|
const uf = document.getElementById("filtro-uf")?.value || "";
|
||||||
|
const url = new URL("/parametros/aliquotas", window.location.origin);
|
||||||
|
if (uf) url.searchParams.set("uf", uf);
|
||||||
|
|
||||||
|
const res = await fetch(url);
|
||||||
const dados = await res.json();
|
const dados = await res.json();
|
||||||
|
|
||||||
const tbody = document.getElementById("tabela-aliquotas");
|
const tbody = document.getElementById("tabela-aliquotas");
|
||||||
|
if (!dados.length) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="3" style="padding:.6rem;">Nenhum registro.</td></tr>`;
|
||||||
|
} else {
|
||||||
tbody.innerHTML = dados.map(a => `
|
tbody.innerHTML = dados.map(a => `
|
||||||
<tr><td>${a.uf}</td><td>${a.exercicio}</td><td>${a.aliquota.toFixed(4)}%</td></tr>
|
<tr>
|
||||||
|
<td>${a.uf}</td>
|
||||||
|
<td>${a.exercicio}</td>
|
||||||
|
<td>${Number(a.aliquota).toLocaleString('pt-BR', {minimumFractionDigits:4, maximumFractionDigits:4})}%</td>
|
||||||
|
<td style="display:flex; gap:8px;">
|
||||||
|
<button class="btn btn-sm btn-secondary"
|
||||||
|
onclick="editarAliquota('${a.uf}', ${a.exercicio}, ${Number(a.aliquota)})">✏️ Editar</button>
|
||||||
|
<button class="btn btn-sm btn-danger"
|
||||||
|
onclick="excluirAliquota('${a.uf}', ${a.exercicio})">🗑️ Excluir</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("total-aliquotas").textContent = `Registros: ${dados.length}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ✅ Eventos após carregar DOM
|
// ✅ Eventos após carregar DOM
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.getElementById("filtro-uf")?.addEventListener("change", carregarAliquotas);
|
||||||
carregarAliquotas();
|
carregarAliquotas();
|
||||||
|
|
||||||
// Ativar/desativar checkbox
|
// Ativar/desativar checkbox
|
||||||
@@ -657,6 +707,86 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function salvarAliquota(form, ev) {
|
||||||
|
ev?.preventDefault();
|
||||||
|
|
||||||
|
const uf = form.uf.value?.trim();
|
||||||
|
const exercicio = Number(form.exercicio.value?.trim());
|
||||||
|
const aliquotaStr = form.aliquota.value?.trim();
|
||||||
|
const aliquota = parseFloat(aliquotaStr.replace(',', '.')); // vírgula -> ponto
|
||||||
|
|
||||||
|
// 👇 LE OS ORIGINAIS
|
||||||
|
const original_uf = document.getElementById('orig-uf').value || null;
|
||||||
|
const original_exercicio = document.getElementById('orig-exercicio').value
|
||||||
|
? Number(document.getElementById('orig-exercicio').value)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!uf || !exercicio || isNaN(exercicio) || !aliquotaStr || isNaN(aliquota)) {
|
||||||
|
mostrarFeedback("❌ Erro", "Preencha UF, exercício e alíquota válidos.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch("/parametros/aliquotas/salvar", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ uf, exercicio, aliquota, original_uf, original_exercicio })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg = await res.text();
|
||||||
|
mostrarFeedback("❌ Erro ao salvar", msg || "Falha na operação.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mostrarFeedback("✅ Salvo", "Alíquota registrada/atualizada com sucesso.");
|
||||||
|
cancelarEdicao(); // limpa modo edição
|
||||||
|
carregarAliquotas();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function editarAliquota(uf, exercicio, aliquota) {
|
||||||
|
const form = document.querySelector('#aliquotas form');
|
||||||
|
form.uf.value = uf;
|
||||||
|
form.exercicio.value = String(exercicio);
|
||||||
|
|
||||||
|
// Mostrar no input com vírgula e 4 casas
|
||||||
|
const valorBR = Number(aliquota).toLocaleString('pt-BR', {
|
||||||
|
minimumFractionDigits: 4, maximumFractionDigits: 4
|
||||||
|
});
|
||||||
|
form.querySelector('[name="aliquota"]').value = valorBR;
|
||||||
|
|
||||||
|
// 👇 GUARDA A CHAVE ORIGINAL
|
||||||
|
document.getElementById('orig-uf').value = uf;
|
||||||
|
document.getElementById('orig-exercicio').value = String(exercicio);
|
||||||
|
|
||||||
|
document.getElementById('aliquotas')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelarEdicao(){
|
||||||
|
const form = document.querySelector('#aliquotas form');
|
||||||
|
form.reset();
|
||||||
|
document.getElementById('orig-uf').value = '';
|
||||||
|
document.getElementById('orig-exercicio').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function excluirAliquota(uf, exercicio){
|
||||||
|
if(!confirm(`Excluir a alíquota de ${uf}/${exercicio}?`)) return;
|
||||||
|
|
||||||
|
const res = await fetch(`/parametros/aliquotas/${encodeURIComponent(uf)}/${exercicio}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!res.ok){
|
||||||
|
const msg = await res.text();
|
||||||
|
mostrarFeedback("❌ Erro", msg || "Falha ao excluir.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mostrarFeedback("🗑️ Excluída", "Alíquota removida com sucesso.");
|
||||||
|
carregarAliquotas();
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Feedback estilo popup -->
|
<!-- Feedback estilo popup -->
|
||||||
|
|||||||
@@ -32,8 +32,10 @@
|
|||||||
<a class="btn btn-secondary" href="/erros/log">📄 Ver Log de Erros (.txt)</a>
|
<a class="btn btn-secondary" href="/erros/log">📄 Ver Log de Erros (.txt)</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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" %}
|
||||||
<button class="btn btn-warning" onclick="limparFaturas()">🧹 Limpar Faturas (Teste)</button>
|
<button class="btn btn-warning" onclick="limparFaturas()">🧹 Limpar Faturas (Teste)</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user