feat: primeira versão da produção
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
root
2025-07-28 13:29:45 -03:00
parent 3aabc7b5c5
commit eeb15d731f
43 changed files with 7779 additions and 0 deletions

90
app/routes/dashboard.py Executable file
View File

@@ -0,0 +1,90 @@
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
from sqlalchemy import create_engine, text
import os
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
# Conexão com o banco de dados PostgreSQL
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/faturas")
engine = create_engine(DATABASE_URL)
@router.get("/dashboard")
def dashboard(request: Request, cliente: str = None):
with engine.connect() as conn:
filtros = ""
if cliente:
filtros = "WHERE nome = :cliente"
# Clientes únicos
clientes_query = text("SELECT DISTINCT nome FROM faturas ORDER BY nome")
clientes = [row[0] for row in conn.execute(clientes_query)]
# Indicadores
indicadores = []
indicadores.append({
"titulo": "Faturas com erro",
"valor": conn.execute(text(f"SELECT COUNT(*) FROM faturas WHERE erro IS TRUE {f'AND nome = :cliente' if cliente else ''}"), {"cliente": cliente} if cliente else {}).scalar()
})
indicadores.append({
"titulo": "Faturas com valor total igual a R$ 0",
"valor": conn.execute(text(f"SELECT COUNT(*) FROM faturas WHERE total = 0 {f'AND nome = :cliente' if cliente else ''}"), {"cliente": cliente} if cliente else {}).scalar()
})
indicadores.append({
"titulo": "Clientes únicos",
"valor": conn.execute(text(f"SELECT COUNT(DISTINCT nome) FROM faturas {filtros}"), {"cliente": cliente} if cliente else {}).scalar()
})
indicadores.append({
"titulo": "Total de faturas",
"valor": conn.execute(text(f"SELECT COUNT(*) FROM faturas {filtros}"), {"cliente": cliente} if cliente else {}).scalar()
})
indicadores.append({
"titulo": "Faturas com campos nulos",
"valor": conn.execute(text(f"SELECT COUNT(*) FROM faturas WHERE base_pis IS NULL OR base_cofins IS NULL OR base_icms IS NULL {f'AND nome = :cliente' if cliente else ''}"), {"cliente": cliente} if cliente else {}).scalar()
})
indicadores.append({
"titulo": "Alíquotas zeradas com valores diferentes de zero",
"valor": conn.execute(text(f"SELECT COUNT(*) FROM faturas WHERE (aliq_pis = 0 AND pis > 0) OR (aliq_cofins = 0 AND cofins > 0) {f'AND nome = :cliente' if cliente else ''}"), {"cliente": cliente} if cliente else {}).scalar()
})
indicadores.append({
"titulo": "Faturas com ICMS incluso após decisão STF",
"valor": conn.execute(text(f"SELECT COUNT(*) FROM faturas WHERE data_emissao > '2017-03-15' AND base_pis = base_icms {f'AND nome = :cliente' if cliente else ''}"), {"cliente": cliente} if cliente else {}).scalar()
})
indicadores.append({
"titulo": "Valor total processado",
"valor": conn.execute(text(f"SELECT ROUND(SUM(total), 2) FROM faturas {filtros}"), {"cliente": cliente} if cliente else {}).scalar() or 0
})
# Análise do STF
def media_percentual_icms(data_inicio, data_fim):
result = conn.execute(text(f"""
SELECT
ROUND(AVG(CASE WHEN base_pis = base_icms THEN 100.0 ELSE 0.0 END), 2) AS percentual_com_icms,
ROUND(AVG(pis + cofins), 2) AS media_valor
FROM faturas
WHERE data_emissao BETWEEN :inicio AND :fim
{f'AND nome = :cliente' if cliente else ''}
"""), {"inicio": data_inicio, "fim": data_fim, "cliente": cliente} if cliente else {"inicio": data_inicio, "fim": data_fim}).mappings().first()
return result or {"percentual_com_icms": 0, "media_valor": 0}
analise_stf = {
"antes": media_percentual_icms("2000-01-01", "2017-03-15"),
"depois": media_percentual_icms("2017-03-16", "2099-12-31")
}
return templates.TemplateResponse("dashboard.html", {
"request": request,
"clientes": clientes,
"cliente_atual": cliente,
"indicadores": indicadores,
"analise_stf": analise_stf
})

32
app/routes/export.py Executable file
View File

@@ -0,0 +1,32 @@
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from models import Fatura
from database import AsyncSessionLocal
import pandas as pd
from io import BytesIO
router = APIRouter()
@router.get("/export-excel")
async def exportar_excel():
async with AsyncSessionLocal() as session:
result = await session.execute(select(Fatura))
faturas = result.scalars().all()
# Converte os objetos para lista de dicionários
data = [f.__dict__ for f in faturas]
for row in data:
row.pop('_sa_instance_state', None) # remove campo interno do SQLAlchemy
df = pd.DataFrame(data)
# Converte para Excel em memória
buffer = BytesIO()
with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer:
df.to_excel(writer, index=False, sheet_name='Faturas')
buffer.seek(0)
return StreamingResponse(buffer, media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={"Content-Disposition": "attachment; filename=faturas.xlsx"})

83
app/routes/parametros.py Executable file
View File

@@ -0,0 +1,83 @@
# parametros.py
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_session
from models import AliquotaUF, ParametrosFormula, SelicMensal
from typing import List
from pydantic import BaseModel
import datetime
router = APIRouter()
# === Schemas ===
class AliquotaUFSchema(BaseModel):
uf: str
exercicio: int
aliquota: float
class Config:
orm_mode = True
class ParametrosFormulaSchema(BaseModel):
nome: str
formula: str
campos: str
class Config:
orm_mode = True
class SelicMensalSchema(BaseModel):
mes: str # 'YYYY-MM'
fator: float
class Config:
orm_mode = True
# === Rotas ===
@router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema])
def listar_aliquotas(db: AsyncSession = Depends(get_session)):
return db.query(AliquotaUF).all()
@router.post("/parametros/aliquotas")
def adicionar_aliquota(aliq: AliquotaUFSchema, db: AsyncSession = Depends(get_session)):
existente = db.query(AliquotaUF).filter_by(uf=aliq.uf, exercicio=aliq.exercicio).first()
if existente:
existente.aliquota = aliq.aliquota
else:
novo = AliquotaUF(**aliq.dict())
db.add(novo)
db.commit()
return {"status": "ok"}
@router.get("/parametros/formulas", response_model=List[ParametrosFormulaSchema])
def listar_formulas(db: AsyncSession = Depends(get_session)):
return db.query(ParametrosFormula).all()
@router.post("/parametros/formulas")
def salvar_formula(form: ParametrosFormulaSchema, db: AsyncSession = Depends(get_session)):
existente = db.query(ParametrosFormula).filter_by(nome=form.nome).first()
if existente:
existente.formula = form.formula
existente.campos = form.campos
else:
novo = ParametrosFormula(**form.dict())
db.add(novo)
db.commit()
return {"status": "ok"}
@router.get("/parametros/selic", response_model=List[SelicMensalSchema])
def listar_selic(db: AsyncSession = Depends(get_session)):
return db.query(SelicMensal).order_by(SelicMensal.mes.desc()).all()
@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"}

84
app/routes/relatorios.py Executable file
View File

@@ -0,0 +1,84 @@
# app/relatorios.py
from fastapi import APIRouter, Depends, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from database import get_session
from models import Fatura, ParametrosFormula, AliquotaUF
from io import BytesIO
import pandas as pd
from datetime import datetime
router = APIRouter()
def calcular_pis_cofins_corretos(base, icms, aliquota):
try:
return round((base - (base - icms)) * aliquota, 5)
except:
return 0.0
@router.get("/relatorio-exclusao-icms")
async def relatorio_exclusao_icms(cliente: str = Query(None), db: AsyncSession = Depends(get_session)):
faturas = db.query(Fatura).all()
dados = []
for f in faturas:
if f.base_pis == f.base_icms == f.base_cofins:
pis_corr = calcular_pis_cofins_corretos(f.base_pis, f.valor_icms, f.aliq_pis)
cofins_corr = calcular_pis_cofins_corretos(f.base_cofins, f.valor_icms, f.aliq_cofins)
dados.append({
"Classificacao": f.classificacao,
"Nome": f.nome,
"UC": f.uc,
"Competencia": f.referencia,
"Valor Total": f.valor_total,
"Alíquota PIS": f.aliq_pis,
"Alíquota ICMS": f.aliq_icms,
"Alíquota COFINS": f.aliq_cofins,
"Valor PIS": f.valor_pis,
"Valor ICMS": f.valor_icms,
"Valor COFINS": f.valor_cofins,
"Base PIS": f.base_pis,
"Base ICMS": f.base_icms,
"PIS Corrigido": pis_corr,
"COFINS Corrigido": cofins_corr,
"Arquivo": f.arquivo
})
df = pd.DataFrame(dados)
excel_file = BytesIO()
df.to_excel(excel_file, index=False)
excel_file.seek(0)
return StreamingResponse(excel_file, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": "attachment; filename=relatorio_exclusao_icms.xlsx"})
@router.get("/relatorio-aliquota-incorreta")
async def relatorio_icms_errado(cliente: str = Query(None), db: AsyncSession = Depends(get_session)):
result = await db.execute(select(Fatura))
faturas = result.scalars().all()
dados = []
for f in faturas:
aliq_registrada = db.query(AliquotaUF).filter_by(uf=f.estado, exercicio=f.referencia[-4:]).first()
if aliq_registrada and abs(f.aliq_icms - aliq_registrada.aliquota) > 0.001:
icms_corr = round((f.base_icms * aliq_registrada.aliquota), 5)
dados.append({
"Classificacao": f.classificacao,
"Nome": f.nome,
"UC": f.uc,
"Competencia": f.referencia,
"Valor Total": f.valor_total,
"Alíquota ICMS (Fatura)": f.aliq_icms,
"Alíquota ICMS (Correta)": aliq_registrada.aliquota,
"Base ICMS": f.base_icms,
"Valor ICMS": f.valor_icms,
"ICMS Corrigido": icms_corr,
"Arquivo": f.arquivo
})
df = pd.DataFrame(dados)
excel_file = BytesIO()
df.to_excel(excel_file, index=False)
excel_file.seek(0)
return StreamingResponse(excel_file, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={"Content-Disposition": "attachment; filename=relatorio_icms_errado.xlsx"})

66
app/routes/selic.py Executable file
View File

@@ -0,0 +1,66 @@
# routes/selic.py
import requests
from fastapi import APIRouter, Query, Depends
from datetime import datetime, timedelta
from sqlalchemy import text
from database import get_session
from models import SelicMensal
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter()
BCB_API_URL = "https://api.bcb.gov.br/dados/serie/bcdata.sgs.4390/dados"
# 🔁 Função reutilizável para startup ou API
async def atualizar_selic_com_base_na_competencia(db: AsyncSession, a_partir_de: str = None):
result = await db.execute(text("SELECT MIN(referencia_competencia) FROM faturas.faturas"))
menor_comp = result.scalar()
if not menor_comp:
return {"message": "Nenhuma fatura encontrada na base."}
inicio = datetime.strptime(a_partir_de, "%m/%Y") if a_partir_de else datetime.strptime(menor_comp, "%m/%Y")
result_ultima = await db.execute(text("SELECT MAX(mes) FROM faturas.selic_mensal"))
ultima = result_ultima.scalar()
fim = datetime.today() if not ultima else max(datetime.today(), ultima + timedelta(days=31))
resultados = []
atual = inicio
while atual <= fim:
mes_ref = atual.replace(day=1)
existe = await db.execute(
text("SELECT 1 FROM faturas.selic_mensal WHERE mes = :mes"),
{"mes": mes_ref}
)
if existe.scalar():
atual += timedelta(days=32)
atual = atual.replace(day=1)
continue
url = f"{BCB_API_URL}?formato=json&dataInicial={mes_ref.strftime('%d/%m/%Y')}&dataFinal={mes_ref.strftime('%d/%m/%Y')}"
r = requests.get(url, timeout=10)
if not r.ok:
atual += timedelta(days=32)
atual = atual.replace(day=1)
continue
dados = r.json()
if dados:
valor = float(dados[0]['valor'].replace(',', '.')) / 100
db.add(SelicMensal(mes=mes_ref, fator=valor))
resultados.append({"mes": mes_ref.strftime("%m/%Y"), "fator": valor})
atual += timedelta(days=32)
atual = atual.replace(day=1)
await db.commit()
return {"message": f"Fatores SELIC atualizados com sucesso.", "novos_registros": resultados}
# 🛠️ Rota opcional reutilizando a função
@router.post("/atualizar-selic")
async def atualizar_selic(
db: AsyncSession = Depends(get_session),
a_partir_de: str = Query(None, description="Opcional: formato MM/AAAA para forçar atualização a partir de determinada data")
):
return await atualizar_selic_com_base_na_competencia(db=db, a_partir_de=a_partir_de)