feat: primeira versão da produção
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
90
app/routes/dashboard.py
Executable file
90
app/routes/dashboard.py
Executable 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
32
app/routes/export.py
Executable 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
83
app/routes/parametros.py
Executable 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
84
app/routes/relatorios.py
Executable 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
66
app/routes/selic.py
Executable 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)
|
||||
Reference in New Issue
Block a user