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:
BIN
app/.main.py.swn
Normal file
BIN
app/.main.py.swn
Normal file
Binary file not shown.
BIN
app/.main.py.swo
Normal file
BIN
app/.main.py.swo
Normal file
Binary file not shown.
BIN
app/.main.py.swp
Normal file
BIN
app/.main.py.swp
Normal file
Binary file not shown.
0
app/CACHED
Normal file
0
app/CACHED
Normal file
22
app/Dockerfile
Normal file
22
app/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
python3-dev \
|
||||||
|
libffi-dev \
|
||||||
|
libmupdf-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"]
|
||||||
0
app/[internal]
Normal file
0
app/[internal]
Normal file
5834
app/app.log
Normal file
5834
app/app.log
Normal file
File diff suppressed because it is too large
Load Diff
92
app/calculos.py
Executable file
92
app/calculos.py
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
# calculos.py
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import datetime
|
||||||
|
from models import ParametrosFormula, SelicMensal
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
import re
|
||||||
|
|
||||||
|
def mes_para_numero(mes: str) -> int:
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if mes.isdigit():
|
||||||
|
return int(mes)
|
||||||
|
return meses.get(mes.upper(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
async def calcular_valor_corrigido(valor_original, competencia: str, session: AsyncSession) -> Decimal:
|
||||||
|
try:
|
||||||
|
mes, ano = competencia.split('/')
|
||||||
|
data_inicio = datetime(int(ano), mes_para_numero(mes), 1)
|
||||||
|
|
||||||
|
query = select(SelicMensal).where(
|
||||||
|
(SelicMensal.ano > data_inicio.year) |
|
||||||
|
((SelicMensal.ano == data_inicio.year) & (SelicMensal.mes >= data_inicio.month))
|
||||||
|
)
|
||||||
|
resultados = await session.execute(query)
|
||||||
|
fatores = resultados.scalars().all()
|
||||||
|
|
||||||
|
fator = Decimal('1.00')
|
||||||
|
for row in fatores:
|
||||||
|
fator *= Decimal(row.fator)
|
||||||
|
|
||||||
|
return Decimal(valor_original) * fator
|
||||||
|
except Exception:
|
||||||
|
return Decimal(valor_original)
|
||||||
|
|
||||||
|
|
||||||
|
async def aplicar_formula(nome: str, contexto: dict, session: AsyncSession) -> Decimal:
|
||||||
|
try:
|
||||||
|
result = await session.execute(
|
||||||
|
select(ParametrosFormula).where(ParametrosFormula.nome == nome)
|
||||||
|
)
|
||||||
|
formula = result.scalar_one_or_none()
|
||||||
|
if not formula:
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
texto_formula = formula.formula # nome correto do campo
|
||||||
|
|
||||||
|
for campo, valor in contexto.items():
|
||||||
|
texto_formula = re.sub(rf'\b{campo}\b', str(valor).replace(',', '.'), texto_formula)
|
||||||
|
|
||||||
|
resultado = eval(texto_formula, {"__builtins__": {}}, {})
|
||||||
|
return Decimal(str(resultado))
|
||||||
|
except Exception:
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
|
||||||
|
async def calcular_campos_dinamicos(fatura: dict, session: AsyncSession) -> dict:
|
||||||
|
try:
|
||||||
|
result = await session.execute(select(ParametrosFormula))
|
||||||
|
parametros = result.scalars().all()
|
||||||
|
|
||||||
|
for param in parametros:
|
||||||
|
try:
|
||||||
|
texto_formula = param.formula
|
||||||
|
for campo, valor in fatura.items():
|
||||||
|
texto_formula = re.sub(rf'\b{campo}\b', str(valor).replace(',', '.'), texto_formula)
|
||||||
|
valor_resultado = eval(texto_formula, {"__builtins__": {}}, {})
|
||||||
|
fatura[param.nome] = round(Decimal(valor_resultado), 2)
|
||||||
|
except:
|
||||||
|
fatura[param.nome] = None
|
||||||
|
|
||||||
|
return fatura
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Erro ao calcular campos dinâmicos: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def calcular_pis_sobre_icms(base_pis, valor_icms, aliq_pis):
|
||||||
|
try:
|
||||||
|
return (Decimal(base_pis) - (Decimal(base_pis) - Decimal(valor_icms))) * Decimal(aliq_pis)
|
||||||
|
except:
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
|
||||||
|
def calcular_cofins_sobre_icms(base_cofins, valor_icms, aliq_cofins):
|
||||||
|
try:
|
||||||
|
return (Decimal(base_cofins) - (Decimal(base_cofins) - Decimal(valor_icms))) * Decimal(aliq_cofins)
|
||||||
|
except:
|
||||||
|
return Decimal('0.00')
|
||||||
15
app/database.py
Executable file
15
app/database.py
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
# database.py
|
||||||
|
DATABASE_URL = "postgresql+asyncpg://fatura:102030@ic-postgresql-FtOY:5432/producao"
|
||||||
|
|
||||||
|
async_engine = create_async_engine(DATABASE_URL, echo=False, future=True)
|
||||||
|
AsyncSessionLocal = sessionmaker(bind=async_engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
async def get_session():
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
yield session
|
||||||
19
app/docker-compose.yml
Normal file
19
app/docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app_fatura:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: Faturas
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- icontainer-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
icontainer-network:
|
||||||
|
external: true
|
||||||
0
app/exporting
Normal file
0
app/exporting
Normal file
132
app/layouts/equatorial_go.py
Executable file
132
app/layouts/equatorial_go.py
Executable file
@@ -0,0 +1,132 @@
|
|||||||
|
# app/layouts/equatorial_go.py
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
import fitz
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def converter_valor(valor_str):
|
||||||
|
try:
|
||||||
|
if not valor_str:
|
||||||
|
return 0.0
|
||||||
|
valor_limpo = str(valor_str).replace('.', '').replace(',', '.')
|
||||||
|
valor_limpo = re.sub(r'[^\d.]', '', valor_limpo)
|
||||||
|
return float(valor_limpo) if valor_limpo else 0.0
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def extrair_dados(texto_final):
|
||||||
|
import logging
|
||||||
|
logging.debug("\n========== INÍCIO DO TEXTO EXTRAÍDO ==========\n" + texto_final + "\n========== FIM ==========")
|
||||||
|
|
||||||
|
def extrair_seguro(patterns, texto_busca, flags=re.IGNORECASE | re.MULTILINE):
|
||||||
|
if not isinstance(patterns, list): patterns = [patterns]
|
||||||
|
for pattern in patterns:
|
||||||
|
match = re.search(pattern, texto_busca, flags)
|
||||||
|
if match:
|
||||||
|
for group in match.groups():
|
||||||
|
if group: return group.strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
nota_fiscal = extrair_seguro(r'NOTA FISCAL Nº\s*(\d+)', texto_final)
|
||||||
|
|
||||||
|
uc = extrair_seguro([
|
||||||
|
r'(\d{7,10}-\d)',
|
||||||
|
r'UNIDADE\s+CONSUMIDORA\s*[:\-]?\s*(\d{6,})',
|
||||||
|
r'(\d{6,})\s+FAZENDA',
|
||||||
|
r'(\d{6,})\s+AVENIDA',
|
||||||
|
r'(\d{6,})\s+RUA'
|
||||||
|
], texto_final)
|
||||||
|
|
||||||
|
logging.debug("TEXTO PDF:\n" + texto_final)
|
||||||
|
|
||||||
|
referencia = extrair_seguro([
|
||||||
|
r'\b([A-Z]{3}/\d{4})\b',
|
||||||
|
r'\b([A-Z]{3}\s*/\s*\d{4})\b',
|
||||||
|
r'\b([A-Z]{3}\d{4})\b',
|
||||||
|
r'(\d{2}/\d{4})'
|
||||||
|
], texto_final.upper())
|
||||||
|
|
||||||
|
# Limpeza prévia para evitar falhas por quebra de linha ou espaços
|
||||||
|
texto_limpo = texto_final.replace('\n', '').replace('\r', '').replace(' ', '')
|
||||||
|
|
||||||
|
if any(padrao in texto_limpo for padrao in ['R$***********0,00', 'R$*********0,00', 'R$*0,00']):
|
||||||
|
valor_total = 0.0
|
||||||
|
else:
|
||||||
|
match_valor_total = re.search(r'R\$[*\s]*([\d\.\s]*,\d{2})', texto_final)
|
||||||
|
valor_total = converter_valor(match_valor_total.group(1)) if match_valor_total else None
|
||||||
|
|
||||||
|
match_nome = re.search(r'(?<=\n)([A-Z\s]{10,})(?=\s+CNPJ/CPF:)', texto_final)
|
||||||
|
nome = match_nome.group(1).replace('\n', ' ').strip() if match_nome else "NÃO IDENTIFICADO"
|
||||||
|
|
||||||
|
# Remove qualquer excesso após o nome verdadeiro
|
||||||
|
nome = re.split(r'\b(FAZENDA|RUA|AVENIDA|SETOR|CEP|CNPJ|CPF)\b', nome, maxsplit=1, flags=re.IGNORECASE)[0].strip()
|
||||||
|
|
||||||
|
match_cidade_estado = re.search(r'CEP:\s*\d{8}\s+(.*?)\s+([A-Z]{2})\s+BRASIL', texto_final)
|
||||||
|
cidade = match_cidade_estado.group(1).strip() if match_cidade_estado else "NÃO IDENTIFICADA"
|
||||||
|
estado = match_cidade_estado.group(2).strip() if match_cidade_estado else "NÃO IDENTIFICADO"
|
||||||
|
|
||||||
|
match_class = re.search(r'Classificação:\s*(.*)', texto_final, re.IGNORECASE)
|
||||||
|
classificacao = match_class.group(1).strip() if match_class else "NÃO IDENTIFICADA"
|
||||||
|
|
||||||
|
def extrair_tributo_linhas_separadas(nome_tributo):
|
||||||
|
linhas = texto_final.split('\n')
|
||||||
|
for i, linha in enumerate(linhas):
|
||||||
|
if nome_tributo in linha.upper():
|
||||||
|
aliq = base = valor = 0.0
|
||||||
|
if i + 1 < len(linhas):
|
||||||
|
aliq_match = re.search(r'([\d,]+)%', linhas[i + 1])
|
||||||
|
if aliq_match:
|
||||||
|
aliq = converter_valor(aliq_match.group(1)) / 100
|
||||||
|
if i + 2 < len(linhas):
|
||||||
|
base = converter_valor(linhas[i + 2].strip())
|
||||||
|
if i + 3 < len(linhas):
|
||||||
|
valor = converter_valor(linhas[i + 3].strip())
|
||||||
|
return base, aliq, valor
|
||||||
|
return 0.0, 0.0, 0.0
|
||||||
|
|
||||||
|
pis_base, pis_aliq, pis_valor = extrair_tributo_linhas_separadas('PIS/PASEP')
|
||||||
|
icms_base, icms_aliq, icms_valor = extrair_tributo_linhas_separadas('ICMS')
|
||||||
|
cofins_base, cofins_aliq, cofins_valor = extrair_tributo_linhas_separadas('COFINS')
|
||||||
|
|
||||||
|
match_consumo = re.search(r'CONSUMO\s+\d+\s+([\d.,]+)', texto_final)
|
||||||
|
consumo = converter_valor(match_consumo.group(1)) if match_consumo else 0.0
|
||||||
|
|
||||||
|
match_tarifa = re.search(r'CONSUMO KWH \+ ICMS/PIS/COFINS\s+([\d.,]+)', texto_final) \
|
||||||
|
or re.search(r'CUSTO DISP\s+([\d.,]+)', texto_final)
|
||||||
|
tarifa = converter_valor(match_tarifa.group(1)) if match_tarifa else 0.0
|
||||||
|
|
||||||
|
dados = {
|
||||||
|
'classificacao_tarifaria': classificacao,
|
||||||
|
'nome': nome,
|
||||||
|
'unidade_consumidora': uc,
|
||||||
|
'cidade': cidade,
|
||||||
|
'estado': estado,
|
||||||
|
'referencia': referencia,
|
||||||
|
'valor_total': valor_total,
|
||||||
|
'pis_aliq': pis_aliq,
|
||||||
|
'icms_aliq': icms_aliq,
|
||||||
|
'cofins_aliq': cofins_aliq,
|
||||||
|
'pis_valor': pis_valor,
|
||||||
|
'icms_valor': icms_valor,
|
||||||
|
'cofins_valor': cofins_valor,
|
||||||
|
'pis_base': pis_base,
|
||||||
|
'icms_base': icms_base,
|
||||||
|
'cofins_base': cofins_base,
|
||||||
|
'consumo': consumo,
|
||||||
|
'tarifa': tarifa,
|
||||||
|
'nota_fiscal': nota_fiscal,
|
||||||
|
'data_processamento': datetime.now(),
|
||||||
|
'distribuidora': 'Equatorial Goiás'
|
||||||
|
}
|
||||||
|
|
||||||
|
campos_obrigatorios = ['nome', 'unidade_consumidora', 'referencia', 'nota_fiscal']
|
||||||
|
faltantes = [campo for campo in campos_obrigatorios if not dados.get(campo)]
|
||||||
|
if valor_total is None and not any(p in texto_limpo for p in ['R$***********0,00', 'R$*********0,00', 'R$*0,00']):
|
||||||
|
faltantes.append('valor_total')
|
||||||
|
|
||||||
|
if faltantes:
|
||||||
|
raise ValueError(f"Campos obrigatórios faltantes: {', '.join(faltantes)}")
|
||||||
|
|
||||||
|
return dados
|
||||||
|
|
||||||
|
|
||||||
197
app/main.py
Normal file
197
app/main.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import uuid
|
||||||
|
from fastapi import FastAPI, Request, UploadFile, File
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
import os, shutil
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from io import BytesIO
|
||||||
|
import pandas as pd
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
from database import AsyncSessionLocal
|
||||||
|
from models import Fatura
|
||||||
|
from models import ParametrosFormula
|
||||||
|
from fastapi import Form
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from processor import (
|
||||||
|
fila_processamento,
|
||||||
|
processar_em_lote,
|
||||||
|
status_arquivos,
|
||||||
|
limpar_arquivos_processados
|
||||||
|
)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
|
UPLOAD_DIR = "uploads/temp"
|
||||||
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
@app.get("/", response_class=HTMLResponse)
|
||||||
|
def dashboard(request: Request):
|
||||||
|
indicadores = [
|
||||||
|
{"titulo": "Total de Faturas", "valor": 124},
|
||||||
|
{"titulo": "Faturas com ICMS", "valor": "63%"},
|
||||||
|
{"titulo": "Valor Total", "valor": "R$ 280.000,00"},
|
||||||
|
]
|
||||||
|
|
||||||
|
analise_stf = {
|
||||||
|
"antes": {"percentual_com_icms": 80, "media_valor": 1200},
|
||||||
|
"depois": {"percentual_com_icms": 20, "media_valor": 800},
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.TemplateResponse("dashboard.html", {
|
||||||
|
"request": request,
|
||||||
|
"cliente_atual": "",
|
||||||
|
"clientes": ["Cliente A", "Cliente B"],
|
||||||
|
"indicadores": indicadores,
|
||||||
|
"analise_stf": analise_stf
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.get("/upload", response_class=HTMLResponse)
|
||||||
|
def upload_page(request: Request):
|
||||||
|
return templates.TemplateResponse("upload.html", {"request": request})
|
||||||
|
|
||||||
|
@app.get("/relatorios", response_class=HTMLResponse)
|
||||||
|
def relatorios_page(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).limit(1))
|
||||||
|
parametros = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
return templates.TemplateResponse("parametros.html", {
|
||||||
|
"request": request,
|
||||||
|
"parametros": parametros or SimpleNamespace(
|
||||||
|
aliquota_icms=None,
|
||||||
|
incluir_icms=True,
|
||||||
|
incluir_pis=True,
|
||||||
|
incluir_cofins=True
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.post("/upload-files")
|
||||||
|
async def upload_files(files: list[UploadFile] = File(...)):
|
||||||
|
for file in files:
|
||||||
|
temp_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}_{file.filename}")
|
||||||
|
with open(temp_path, "wb") as f:
|
||||||
|
shutil.copyfileobj(file.file, f)
|
||||||
|
await fila_processamento.put({
|
||||||
|
"caminho_pdf": temp_path,
|
||||||
|
"nome_original": file.filename
|
||||||
|
})
|
||||||
|
return {"message": "Arquivos enviados para fila"}
|
||||||
|
|
||||||
|
@app.post("/process-queue")
|
||||||
|
async def process_queue():
|
||||||
|
resultados = await processar_em_lote()
|
||||||
|
return {"message": "Processamento concluído", "resultados": resultados}
|
||||||
|
|
||||||
|
@app.get("/get-status")
|
||||||
|
async def get_status():
|
||||||
|
files = []
|
||||||
|
for nome, status in status_arquivos.items():
|
||||||
|
files.append({
|
||||||
|
"nome": nome,
|
||||||
|
"status": status,
|
||||||
|
"mensagem": "---" if status == "Concluído" else status
|
||||||
|
})
|
||||||
|
is_processing = not fila_processamento.empty()
|
||||||
|
return JSONResponse(content={"is_processing": is_processing, "files": files})
|
||||||
|
|
||||||
|
@app.post("/clear-all")
|
||||||
|
async def clear_all():
|
||||||
|
limpar_arquivos_processados()
|
||||||
|
for f in os.listdir(UPLOAD_DIR):
|
||||||
|
os.remove(os.path.join(UPLOAD_DIR, f))
|
||||||
|
return {"message": "Fila e arquivos limpos"}
|
||||||
|
|
||||||
|
@app.get("/export-excel")
|
||||||
|
async def export_excel():
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
result = await session.execute(select(Fatura))
|
||||||
|
faturas = result.scalars().all()
|
||||||
|
|
||||||
|
dados = []
|
||||||
|
for f in faturas:
|
||||||
|
dados.append({
|
||||||
|
"Nome": f.nome,
|
||||||
|
"UC": f.unidade_consumidora,
|
||||||
|
"Referência": f.referencia,
|
||||||
|
"Nota Fiscal": f.nota_fiscal,
|
||||||
|
"Valor Total": f.valor_total,
|
||||||
|
"ICMS (%)": f.icms_aliq,
|
||||||
|
"ICMS (R$)": f.icms_valor,
|
||||||
|
"Base ICMS": f.icms_base,
|
||||||
|
"PIS (%)": f.pis_aliq,
|
||||||
|
"PIS (R$)": f.pis_valor,
|
||||||
|
"Base PIS": f.pis_base,
|
||||||
|
"COFINS (%)": f.cofins_aliq,
|
||||||
|
"COFINS (R$)": f.cofins_valor,
|
||||||
|
"Base COFINS": f.cofins_base,
|
||||||
|
"Consumo (kWh)": f.consumo,
|
||||||
|
"Tarifa": f.tarifa,
|
||||||
|
"Cidade": f.cidade,
|
||||||
|
"Estado": f.estado,
|
||||||
|
"Distribuidora": f.distribuidora,
|
||||||
|
"Data Processamento": f.data_processamento,
|
||||||
|
})
|
||||||
|
|
||||||
|
df = pd.DataFrame(dados)
|
||||||
|
output = BytesIO()
|
||||||
|
df.to_excel(output, index=False, sheet_name="Faturas")
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
output,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={"Content-Disposition": "attachment; filename=relatorio_faturas.xlsx"}
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.post("/parametros", response_class=HTMLResponse)
|
||||||
|
async def salvar_parametros(
|
||||||
|
request: Request,
|
||||||
|
aliquota_icms: float = Form(...),
|
||||||
|
formula_pis: str = Form(...),
|
||||||
|
formula_cofins: str = Form(...)
|
||||||
|
):
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
result = await session.execute(select(ParametrosFormula).limit(1))
|
||||||
|
existente = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if existente:
|
||||||
|
existente.aliquota_icms = aliquota_icms
|
||||||
|
existente.incluir_icms = 1
|
||||||
|
existente.incluir_pis = 1
|
||||||
|
existente.incluir_cofins = 1
|
||||||
|
existente.formula = f"PIS={formula_pis};COFINS={formula_cofins}"
|
||||||
|
mensagem = "Parâmetros atualizados com sucesso."
|
||||||
|
else:
|
||||||
|
novo = ParametrosFormula(
|
||||||
|
aliquota_icms=aliquota_icms,
|
||||||
|
incluir_icms=1,
|
||||||
|
incluir_pis=1,
|
||||||
|
incluir_cofins=1,
|
||||||
|
formula=f"PIS={formula_pis};COFINS={formula_cofins}"
|
||||||
|
)
|
||||||
|
session.add(novo)
|
||||||
|
mensagem = "Parâmetros salvos com sucesso."
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
parametros = SimpleNamespace(
|
||||||
|
aliquota_icms=aliquota_icms,
|
||||||
|
incluir_icms=1,
|
||||||
|
incluir_pis=1,
|
||||||
|
incluir_cofins=1,
|
||||||
|
formula_pis=formula_pis,
|
||||||
|
formula_cofins=formula_cofins
|
||||||
|
)
|
||||||
|
|
||||||
|
return templates.TemplateResponse("parametros.html", {
|
||||||
|
"request": request,
|
||||||
|
"parametros": parametros,
|
||||||
|
"mensagem": mensagem
|
||||||
|
})
|
||||||
BIN
app/modelos_fatura/equatorial_goias_2025.pdf
Normal file
BIN
app/modelos_fatura/equatorial_goias_2025.pdf
Normal file
Binary file not shown.
BIN
app/modelos_fatura/equatorial_para_2025.pdf
Normal file
BIN
app/modelos_fatura/equatorial_para_2025.pdf
Normal file
Binary file not shown.
85
app/models.py
Executable file
85
app/models.py
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
# 📄 models.py
|
||||||
|
from sqlalchemy import Column, String, Integer, Float, DateTime, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from database import Base
|
||||||
|
|
||||||
|
class ParametrosFormula(Base):
|
||||||
|
__tablename__ = 'parametros_formula'
|
||||||
|
__table_args__ = {'schema': 'faturas', 'extend_existing': True}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nome = Column(String)
|
||||||
|
formula = Column(Text)
|
||||||
|
|
||||||
|
# Novos campos
|
||||||
|
aliquota_icms = Column(Float)
|
||||||
|
incluir_icms = Column(Integer) # Use Boolean se preferir
|
||||||
|
incluir_pis = Column(Integer)
|
||||||
|
incluir_cofins = Column(Integer)
|
||||||
|
|
||||||
|
class Fatura(Base):
|
||||||
|
__tablename__ = "faturas"
|
||||||
|
__table_args__ = {'schema': 'faturas'}
|
||||||
|
|
||||||
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||||
|
nome = Column(String)
|
||||||
|
classificacao_tarifaria = Column("classificacao_tarifaria", String)
|
||||||
|
unidade_consumidora = Column("unidade_consumidora", String)
|
||||||
|
referencia = Column(String)
|
||||||
|
valor_total = Column(Float)
|
||||||
|
|
||||||
|
pis_aliq = Column("pis_aliq", Float)
|
||||||
|
pis_valor = Column("pis_valor", Float)
|
||||||
|
pis_base = Column("pis_base", Float)
|
||||||
|
|
||||||
|
icms_aliq = Column("icms_aliq", Float)
|
||||||
|
icms_valor = Column("icms_valor", Float)
|
||||||
|
icms_base = Column("icms_base", Float)
|
||||||
|
|
||||||
|
cofins_aliq = Column("cofins_aliq", Float)
|
||||||
|
cofins_valor = Column("cofins_valor", Float)
|
||||||
|
cofins_base = Column("cofins_base", Float)
|
||||||
|
|
||||||
|
consumo = Column("consumo", Float)
|
||||||
|
tarifa = Column("tarifa", Float)
|
||||||
|
|
||||||
|
nota_fiscal = Column(String)
|
||||||
|
data_processamento = Column(DateTime, default=datetime.utcnow)
|
||||||
|
arquivo_pdf = Column("arquivo_pdf", String)
|
||||||
|
cidade = Column(String)
|
||||||
|
estado = Column(String)
|
||||||
|
distribuidora = Column(String)
|
||||||
|
link_arquivo = Column("link_arquivo", String)
|
||||||
|
|
||||||
|
|
||||||
|
class LogProcessamento(Base):
|
||||||
|
__tablename__ = "logs_processamento"
|
||||||
|
__table_args__ = {'schema': 'faturas'}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nome_arquivo = Column(String)
|
||||||
|
status = Column(String)
|
||||||
|
mensagem = Column(Text)
|
||||||
|
acao = Column(String) # nova coluna existente no banco
|
||||||
|
data_log = Column("data_log", DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class AliquotaUF(Base):
|
||||||
|
__tablename__ = "aliquotas_uf"
|
||||||
|
__table_args__ = {'schema': 'faturas'}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
uf = Column(String)
|
||||||
|
exercicio = Column(String)
|
||||||
|
aliquota = Column(Float)
|
||||||
|
|
||||||
|
class SelicMensal(Base):
|
||||||
|
__tablename__ = "selic_mensal"
|
||||||
|
__table_args__ = {'schema': 'faturas'}
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
ano = Column(Integer)
|
||||||
|
mes = Column(Integer)
|
||||||
|
fator = Column(Float)
|
||||||
0
app/naming
Normal file
0
app/naming
Normal file
BIN
app/planilha_faturas.xlsx
Normal file
BIN
app/planilha_faturas.xlsx
Normal file
Binary file not shown.
93
app/processor.py
Normal file
93
app/processor.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import asyncio
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
from utils import extrair_dados_pdf
|
||||||
|
from database import AsyncSessionLocal
|
||||||
|
from models import Fatura, LogProcessamento
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UPLOADS_DIR = os.path.join(os.getcwd(), "uploads")
|
||||||
|
TEMP_DIR = os.path.join(UPLOADS_DIR, "temp")
|
||||||
|
|
||||||
|
fila_processamento = asyncio.Queue()
|
||||||
|
status_arquivos = {}
|
||||||
|
|
||||||
|
def remover_arquivo_temp(caminho_pdf):
|
||||||
|
try:
|
||||||
|
if os.path.exists(caminho_pdf) and TEMP_DIR in caminho_pdf:
|
||||||
|
os.remove(caminho_pdf)
|
||||||
|
logger.info(f"Arquivo temporário removido: {os.path.basename(caminho_pdf)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Falha ao remover arquivo temporário: {e}")
|
||||||
|
|
||||||
|
def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
|
||||||
|
try:
|
||||||
|
extensao = os.path.splitext(nome_original)[1].lower()
|
||||||
|
nome_destino = f"{nota_fiscal}{extensao}"
|
||||||
|
destino_final = os.path.join(UPLOADS_DIR, nome_destino)
|
||||||
|
shutil.copy2(caminho_pdf_temp, destino_final)
|
||||||
|
return destino_final
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erro ao salvar em uploads: {e}")
|
||||||
|
return caminho_pdf_temp
|
||||||
|
|
||||||
|
async def process_single_file(caminho_pdf_temp: str, nome_original: str):
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
try:
|
||||||
|
dados = extrair_dados_pdf(caminho_pdf_temp)
|
||||||
|
dados['arquivo_pdf'] = nome_original
|
||||||
|
|
||||||
|
existente_result = await session.execute(
|
||||||
|
select(Fatura).filter_by(nota_fiscal=dados['nota_fiscal'], unidade_consumidora=dados['unidade_consumidora'])
|
||||||
|
)
|
||||||
|
if existente_result.scalar_one_or_none():
|
||||||
|
remover_arquivo_temp(caminho_pdf_temp)
|
||||||
|
return {"status": "Duplicado", "dados": dados}
|
||||||
|
|
||||||
|
caminho_final = salvar_em_uploads(caminho_pdf_temp, nome_original, dados['nota_fiscal'])
|
||||||
|
dados['link_arquivo'] = caminho_final
|
||||||
|
|
||||||
|
|
||||||
|
fatura = Fatura(**dados)
|
||||||
|
session.add(fatura)
|
||||||
|
|
||||||
|
session.add(LogProcessamento(
|
||||||
|
status="Sucesso",
|
||||||
|
mensagem="Fatura processada com sucesso",
|
||||||
|
nome_arquivo=nome_original,
|
||||||
|
acao="PROCESSAMENTO"
|
||||||
|
))
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
remover_arquivo_temp(caminho_pdf_temp)
|
||||||
|
return {"status": "Concluído", "dados": dados}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
session.add(LogProcessamento(
|
||||||
|
status="Erro",
|
||||||
|
mensagem=str(e),
|
||||||
|
nome_arquivo=nome_original,
|
||||||
|
acao="PROCESSAMENTO"
|
||||||
|
))
|
||||||
|
await session.commit()
|
||||||
|
logger.error(f"Erro ao processar fatura: {e}", exc_info=True)
|
||||||
|
remover_arquivo_temp(caminho_pdf_temp)
|
||||||
|
return {"status": "Erro", "mensagem": str(e)}
|
||||||
|
|
||||||
|
async def processar_em_lote():
|
||||||
|
resultados = []
|
||||||
|
while not fila_processamento.empty():
|
||||||
|
item = await fila_processamento.get()
|
||||||
|
resultado = await process_single_file(item['caminho_pdf'], item['nome_original'])
|
||||||
|
status_arquivos[item['nome_original']] = resultado.get("status", "Erro")
|
||||||
|
resultados.append(resultado)
|
||||||
|
return resultados
|
||||||
|
|
||||||
|
def limpar_arquivos_processados():
|
||||||
|
status_arquivos.clear()
|
||||||
|
while not fila_processamento.empty():
|
||||||
|
fila_processamento.get_nowait()
|
||||||
0
app/reading
Normal file
0
app/reading
Normal file
79
app/relatorio_processamento.txt
Normal file
79
app/relatorio_processamento.txt
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
Relatório de Processamento - 21/07/2025 21:05:12
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Arquivo: 2020017770587.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020017828307.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020016069139.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020017302737.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020012045591.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020012935068.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020012935072.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020012935076.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020013384559.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020011592594.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020017828322.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020017828326.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020018729075.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020019161184.pdf
|
||||||
|
Status: Erro
|
||||||
|
Mensagem: Erro ao processar PDF: Campos obrigatórios faltantes: valor_total
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020022723459.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020025522861.pdf
|
||||||
|
Status: Erro
|
||||||
|
Mensagem: Erro ao processar PDF: Campos obrigatórios faltantes: valor_total
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020028540785.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020028589110.pdf
|
||||||
|
Status: Duplicado
|
||||||
|
Mensagem: Fatura já processada anteriormente.
|
||||||
|
--------------------------------------------------
|
||||||
|
Arquivo: 2020028649330.pdf
|
||||||
|
Status: Erro
|
||||||
|
Mensagem: Erro ao processar PDF: Campos obrigatórios faltantes: valor_total
|
||||||
|
--------------------------------------------------
|
||||||
9
app/requirements.txt
Normal file
9
app/requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
fastapi==0.110.0
|
||||||
|
uvicorn[standard]==0.29.0
|
||||||
|
jinja2==3.1.3
|
||||||
|
sqlalchemy==2.0.30
|
||||||
|
asyncpg==0.29.0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
openpyxl==3.1.2
|
||||||
|
pandas==2.2.2
|
||||||
|
PyMuPDF==1.22.5
|
||||||
0
app/resolving
Normal file
0
app/resolving
Normal file
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)
|
||||||
10
app/startup.py
Executable file
10
app/startup.py
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
# startup.py
|
||||||
|
import logging
|
||||||
|
from routes.selic import atualizar_selic_com_base_na_competencia
|
||||||
|
|
||||||
|
async def executar_rotinas_iniciais(db):
|
||||||
|
try:
|
||||||
|
await atualizar_selic_com_base_na_competencia(db)
|
||||||
|
logging.info("✅ Tabela SELIC atualizada com sucesso na inicialização.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Erro ao atualizar SELIC na inicialização: {str(e)}")
|
||||||
6
app/static/cloud-upload.svg
Normal file
6
app/static/cloud-upload.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="none" stroke="#374151" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-upload-cloud" viewBox="0 0 24 24">
|
||||||
|
<path d="M16 16l-4-4-4 4"></path>
|
||||||
|
<path d="M12 12v9"></path>
|
||||||
|
<path d="M20.39 18.39A5.5 5.5 0 0 0 18 9h-1.26A8 8 0 1 0 4 16.3"></path>
|
||||||
|
<path d="M16 16l-4-4-4 4"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 397 B |
107
app/templates/dashboard.html
Executable file
107
app/templates/dashboard.html
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Dashboard{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1 style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<i class="fas fa-chart-line"></i> Dashboard de Faturas
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form method="get" style="margin: 20px 0;">
|
||||||
|
<label for="cliente">Selecionar Cliente:</label>
|
||||||
|
<select name="cliente" id="cliente" onchange="this.form.submit()">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
{% for c in clientes %}
|
||||||
|
<option value="{{ c }}" {% if c == cliente_atual %}selected{% endif %}>{{ c }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Cards -->
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 30px;">
|
||||||
|
{% for indicador in indicadores %}
|
||||||
|
<div style="
|
||||||
|
flex: 1 1 220px;
|
||||||
|
background: #2563eb;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
||||||
|
">
|
||||||
|
<strong>{{ indicador.titulo }}</strong>
|
||||||
|
<div style="font-size: 1.6rem; font-weight: bold; margin-top: 10px;">
|
||||||
|
{{ indicador.valor }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 style="margin-bottom: 20px;"><i class="fas fa-chart-bar"></i> Análise da Decisão do STF (RE 574.706 – 15/03/2017)</h2>
|
||||||
|
|
||||||
|
<div style="display: flex; flex-wrap: wrap; gap: 20px;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<h4>% de Faturas com ICMS na Base PIS/COFINS</h4>
|
||||||
|
<canvas id="graficoICMS"></canvas>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<h4>Valor Médio de Tributos com ICMS</h4>
|
||||||
|
<canvas id="graficoValor"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
const ctx1 = document.getElementById('graficoICMS').getContext('2d');
|
||||||
|
new Chart(ctx1, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Antes da Decisão', 'Depois da Decisão'],
|
||||||
|
datasets: [{
|
||||||
|
label: '% com ICMS na Base',
|
||||||
|
data: {{ [analise_stf.antes.percentual_com_icms, analise_stf.depois.percentual_com_icms] | tojson }},
|
||||||
|
backgroundColor: ['#f39c12', '#e74c3c']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true },
|
||||||
|
title: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: { display: true, text: '%' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx2 = document.getElementById('graficoValor').getContext('2d');
|
||||||
|
new Chart(ctx2, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Antes da Decisão', 'Depois da Decisão'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Valor Médio de PIS/COFINS com ICMS',
|
||||||
|
data: {{ [analise_stf.antes.media_valor, analise_stf.depois.media_valor] | tojson }},
|
||||||
|
backgroundColor: ['#2980b9', '#27ae60']
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true },
|
||||||
|
title: { display: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: { display: true, text: 'R$' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
184
app/templates/index.html
Normal file
184
app/templates/index.html
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>{% block title %}ProcessaWatt{% endblock %}</title>
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡️</text></svg>">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #2563eb;
|
||||||
|
--primary-dark: #1e40af;
|
||||||
|
--sidebar-width: 250px;
|
||||||
|
--sidebar-collapsed: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Montserrat', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f8fafc;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: var(--sidebar-collapsed);
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(to bottom, var(--primary), var(--primary-dark));
|
||||||
|
position: fixed;
|
||||||
|
transition: all 0.3s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.expanded {
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
gap: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.expanded .app-name {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 50px;
|
||||||
|
color: rgba(255,255,255,0.9);
|
||||||
|
text-decoration: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item i {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item span {
|
||||||
|
margin-left: 15px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.expanded .menu-item span {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: var(--sidebar-collapsed);
|
||||||
|
padding: 30px;
|
||||||
|
flex-grow: 1;
|
||||||
|
transition: margin-left 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.expanded ~ .main-content {
|
||||||
|
margin-left: var(--sidebar-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
padding: 10px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 30px 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar" id="sidebar">
|
||||||
|
<div class="logo-container" onclick="toggleSidebar()">
|
||||||
|
<div class="logo-icon">⚡️</div>
|
||||||
|
<div class="app-name">ProcessaWatt</div>
|
||||||
|
</div>
|
||||||
|
<div class="menu">
|
||||||
|
<a href="/" class="menu-item">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
<a href="/upload" class="menu-item">
|
||||||
|
<i class="fas fa-upload"></i>
|
||||||
|
<span>Upload</span>
|
||||||
|
</a>
|
||||||
|
<a href="/relatorios" class="menu-item">
|
||||||
|
<i class="fas fa-chart-bar"></i>
|
||||||
|
<span>Relatórios</span>
|
||||||
|
</a>
|
||||||
|
<a href="/parametros" class="menu-item">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
<span>Parâmetros</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="main-content">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleSidebar() {
|
||||||
|
document.getElementById('sidebar').classList.toggle('expanded');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
201
app/templates/index2.html
Executable file
201
app/templates/index2.html
Executable file
@@ -0,0 +1,201 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>Processador de Faturas</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600&display=swap" rel="stylesheet"/>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Montserrat', sans-serif; }
|
||||||
|
body { background-color: #f7f9fc; padding: 2rem; color: #333; }
|
||||||
|
|
||||||
|
.nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
|
||||||
|
.nav h1 { font-size: 1.5rem; color: #4361ee; }
|
||||||
|
.nav ul { display: flex; list-style: none; gap: 1.5rem; }
|
||||||
|
.nav li a { text-decoration: none; color: #333; font-weight: 600; }
|
||||||
|
.nav li a:hover { color: #4361ee; }
|
||||||
|
|
||||||
|
.upload-box {
|
||||||
|
background: #fff;
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.upload-box.dragover {
|
||||||
|
background-color: #eef2ff;
|
||||||
|
border-color: #4361ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin-bottom: 2rem; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.btn:hover { opacity: 0.9; }
|
||||||
|
.btn-primary { background-color: #4361ee; color: white; }
|
||||||
|
.btn-success { background-color: #198754; color: white; }
|
||||||
|
.btn-danger { background-color: #dc3545; color: white; }
|
||||||
|
.btn-secondary { background-color: #6c757d; color: white; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #555;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-input { display: none; }
|
||||||
|
|
||||||
|
.status-ok { color: #198754; }
|
||||||
|
.status-error { color: #dc3545; }
|
||||||
|
.status-warn { color: #ffc107; }
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 3rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<h1>📄 Processador de Faturas</h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">Upload</a></li>
|
||||||
|
<li><a href="/dashboard">Dashboard</a></li>
|
||||||
|
<li><a href="/relatorios">Relatórios</a></li>
|
||||||
|
<li><a href="/parametros">Parâmetros</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="upload-box" id="upload-box">
|
||||||
|
<h3>Arraste faturas em PDF aqui ou clique para selecionar</h3>
|
||||||
|
<p style="color: gray; font-size: 0.9rem;">Apenas PDFs textuais (não escaneados)</p>
|
||||||
|
<br />
|
||||||
|
<button class="btn btn-primary" onclick="document.getElementById('file-input').click()">Selecionar Arquivos</button>
|
||||||
|
<input type="file" id="file-input" accept=".pdf" multiple onchange="handleFiles(this.files)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn btn-primary" onclick="processar()">Processar Faturas</button>
|
||||||
|
<button class="btn btn-danger" onclick="limpar()">Limpar Tudo</button>
|
||||||
|
<button class="btn btn-success" onclick="baixarPlanilha()">📥 Abrir Planilha</button>
|
||||||
|
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Arquivo</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Mensagem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="file-table">
|
||||||
|
<tr><td colspan="3" style="text-align: center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Sistema desenvolvido para análise tributária de faturas (PIS/COFINS/ICMS) com correção SELIC.
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let arquivos = [];
|
||||||
|
let statusInterval = null;
|
||||||
|
const fileTable = document.getElementById('file-table');
|
||||||
|
|
||||||
|
function handleFiles(files) {
|
||||||
|
arquivos = [...arquivos, ...files];
|
||||||
|
renderTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(statusList = []) {
|
||||||
|
const rows = statusList.length ? statusList : arquivos.map(file => ({ nome: file.name, status: 'Aguardando', mensagem: '' }));
|
||||||
|
fileTable.innerHTML = rows.length
|
||||||
|
? rows.map(file => `
|
||||||
|
<tr>
|
||||||
|
<td>${file.nome}</td>
|
||||||
|
<td class="${file.status === 'Concluido' ? 'status-ok' : file.status === 'Erro' ? 'status-error' : 'status-warn'}">${file.status}</td>
|
||||||
|
<td>${file.mensagem || '---'}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')
|
||||||
|
: '<tr><td colspan="3" style="text-align:center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processar() {
|
||||||
|
if (arquivos.length === 0) return alert("Nenhum arquivo selecionado.");
|
||||||
|
const formData = new FormData();
|
||||||
|
arquivos.forEach(file => formData.append("files", file));
|
||||||
|
|
||||||
|
await fetch("/upload-files", { method: "POST", body: formData });
|
||||||
|
await fetch("/process-queue", { method: "POST" });
|
||||||
|
arquivos = [];
|
||||||
|
statusInterval = setInterval(updateStatus, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateStatus() {
|
||||||
|
const res = await fetch("/get-status");
|
||||||
|
const data = await res.json();
|
||||||
|
renderTable(data.files);
|
||||||
|
|
||||||
|
if (!data.is_processing && statusInterval) {
|
||||||
|
clearInterval(statusInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function limpar() {
|
||||||
|
fetch("/clear-all", { method: "POST" });
|
||||||
|
arquivos = [];
|
||||||
|
renderTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function baixarPlanilha() {
|
||||||
|
window.open('/download-spreadsheet', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function gerarRelatorio() {
|
||||||
|
window.open('/generate-report', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropZone = document.getElementById('upload-box');
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('dragover');
|
||||||
|
});
|
||||||
|
dropZone.addEventListener('dragleave', () => {
|
||||||
|
dropZone.classList.remove('dragover');
|
||||||
|
});
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('dragover');
|
||||||
|
handleFiles(e.dataTransfer.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', updateStatus);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
32
app/templates/parametros.html
Executable file
32
app/templates/parametros.html
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Parâmetros de Cálculo{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>⚙️ Parâmetros</h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<label for="tipo">Tipo:</label><br>
|
||||||
|
<input type="text" name="tipo" id="tipo" value="{{ parametros.tipo or '' }}" required/><br><br>
|
||||||
|
|
||||||
|
<label for="formula">Fórmula:</label><br>
|
||||||
|
<input type="text" name="formula" id="formula" value="{{ parametros.formula or '' }}" required/><br><br>
|
||||||
|
|
||||||
|
<label for="aliquota_icms">Alíquota de ICMS (%):</label><br>
|
||||||
|
<input type="number" step="0.01" name="aliquota_icms" id="aliquota_icms" value="{{ parametros.aliquota_icms or '' }}" /><br><br>
|
||||||
|
|
||||||
|
<label for="incluir_icms">Incluir ICMS:</label><br>
|
||||||
|
<input type="checkbox" name="incluir_icms" id="incluir_icms" value="1" {% if parametros.incluir_icms %}checked{% endif %}><br><br>
|
||||||
|
|
||||||
|
<label for="ativo">Ativo:</label><br>
|
||||||
|
<input type="checkbox" name="ativo" id="ativo" value="1" {% if parametros.ativo %}checked{% endif %}><br><br>
|
||||||
|
|
||||||
|
<button type="submit" style="padding: 10px 20px; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
Salvar Parâmetros
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if mensagem %}
|
||||||
|
<div style="margin-top: 20px; background: #e0f7fa; padding: 10px; border-left: 4px solid #2563eb;">
|
||||||
|
{{ mensagem }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
38
app/templates/relatorios.html
Executable file
38
app/templates/relatorios.html
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Relatórios{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>📊 Relatórios</h1>
|
||||||
|
|
||||||
|
<form method="get" style="margin-bottom: 20px;">
|
||||||
|
<label for="cliente">Filtrar por Cliente:</label>
|
||||||
|
<select name="cliente" id="cliente" onchange="this.form.submit()">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
{% for c in clientes %}
|
||||||
|
<option value="{{ c }}" {% if c == cliente_atual %}selected{% endif %}>{{ c }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: #2563eb; color: white;">
|
||||||
|
<th style="padding: 10px;">Cliente</th>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Valor Total</th>
|
||||||
|
<th>ICMS na Base</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for f in faturas %}
|
||||||
|
<tr style="background: {{ loop.cycle('#ffffff', '#f0f4f8') }};">
|
||||||
|
<td style="padding: 10px;">{{ f.nome }}</td>
|
||||||
|
<td>{{ f.data_emissao }}</td>
|
||||||
|
<td>R$ {{ '%.2f'|format(f.valor_total)|replace('.', ',') }}</td>
|
||||||
|
<td>{{ 'Sim' if f.com_icms else 'Não' }}</td>
|
||||||
|
<td>{{ f.status }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
184
app/templates/upload.html
Executable file
184
app/templates/upload.html
Executable file
@@ -0,0 +1,184 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Upload de Faturas{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1 style="font-size: 1.5rem; margin-bottom: 1rem;">📤 Upload de Faturas</h1>
|
||||||
|
|
||||||
|
<div class="upload-box" id="upload-box">
|
||||||
|
<h3>Arraste faturas em PDF aqui ou clique para selecionar</h3>
|
||||||
|
<p style="color: gray; font-size: 0.9rem;">Apenas PDFs textuais (não escaneados)</p>
|
||||||
|
<br />
|
||||||
|
<button class="btn btn-primary" onclick="document.getElementById('file-input').click()">Selecionar Arquivos</button>
|
||||||
|
<input type="file" id="file-input" accept=".pdf" multiple onchange="handleFiles(this.files)" style="display:none;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn btn-primary" onclick="processar(this)">Processar Faturas</button>
|
||||||
|
<button class="btn btn-danger" onclick="limpar()">Limpar Tudo</button>
|
||||||
|
<button class="btn btn-success" onclick="baixarPlanilha()">📅 Abrir Planilha</button>
|
||||||
|
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Arquivo</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Mensagem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="file-table">
|
||||||
|
<tr><td colspan="3" style="text-align: center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let arquivos = [];
|
||||||
|
let statusInterval = null;
|
||||||
|
const fileTable = document.getElementById('file-table');
|
||||||
|
|
||||||
|
function handleFiles(files) {
|
||||||
|
arquivos = [...arquivos, ...files];
|
||||||
|
renderTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(statusList = []) {
|
||||||
|
const rows = statusList.length ? statusList : arquivos.map(file => ({ nome: file.name, status: 'Aguardando', mensagem: '' }));
|
||||||
|
fileTable.innerHTML = rows.length
|
||||||
|
? rows.map(file => `
|
||||||
|
<tr>
|
||||||
|
<td>${file.nome}</td>
|
||||||
|
<td class="${file.status === 'Concluido' ? 'status-ok' : file.status === 'Erro' ? 'status-error' : 'status-warn'}">${file.status}</td>
|
||||||
|
<td>${file.mensagem || '---'}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')
|
||||||
|
: '<tr><td colspan="3" style="text-align:center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processar(btn) {
|
||||||
|
if (arquivos.length === 0) return alert("Nenhum arquivo selecionado.");
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerText = "⏳ Processando...";
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
arquivos.forEach(file => formData.append("files", file));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch("/upload-files", { method: "POST", body: formData });
|
||||||
|
await fetch("/process-queue", { method: "POST" });
|
||||||
|
arquivos = [];
|
||||||
|
statusInterval = setInterval(updateStatus, 1000);
|
||||||
|
} catch (err) {
|
||||||
|
alert("Erro ao processar faturas.");
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerText = "Processar Faturas";
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateStatus() {
|
||||||
|
const res = await fetch("/get-status");
|
||||||
|
const data = await res.json();
|
||||||
|
renderTable(data.files);
|
||||||
|
|
||||||
|
if (!data.is_processing && statusInterval) {
|
||||||
|
clearInterval(statusInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function limpar() {
|
||||||
|
fetch("/clear-all", { method: "POST" });
|
||||||
|
arquivos = [];
|
||||||
|
renderTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function baixarPlanilha() {
|
||||||
|
window.open('/export-excel', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function gerarRelatorio() {
|
||||||
|
window.open('/generate-report', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropZone = document.getElementById('upload-box');
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('dragover');
|
||||||
|
});
|
||||||
|
dropZone.addEventListener('dragleave', () => {
|
||||||
|
dropZone.classList.remove('dragover');
|
||||||
|
});
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('dragover');
|
||||||
|
handleFiles(e.dataTransfer.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', updateStatus);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.upload-box {
|
||||||
|
background: #fff;
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.upload-box.dragover {
|
||||||
|
background-color: #eef2ff;
|
||||||
|
border-color: #4361ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.btn:hover { opacity: 0.9; }
|
||||||
|
.btn-primary { background-color: #4361ee; color: white; }
|
||||||
|
.btn-success { background-color: #198754; color: white; }
|
||||||
|
.btn-danger { background-color: #dc3545; color: white; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #555;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-ok { color: #198754; }
|
||||||
|
.status-error { color: #dc3545; }
|
||||||
|
.status-warn { color: #ffc107; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
20
app/testar_fatura.py
Executable file
20
app/testar_fatura.py
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
import asyncio
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
DATABASE_URL = "postgresql+asyncpg://fatura:102030@ic-postgresql-FtOY:5432/app_faturas"
|
||||||
|
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||||
|
|
||||||
|
async def testar_conexao():
|
||||||
|
try:
|
||||||
|
async with engine.connect() as conn:
|
||||||
|
result = await conn.execute(text("SELECT 1"))
|
||||||
|
row = await result.fetchone()
|
||||||
|
print("Resultado:", row)
|
||||||
|
except Exception as e:
|
||||||
|
print("Erro ao conectar no banco:", e)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(testar_conexao())
|
||||||
|
|
||||||
0
app/transferring
Normal file
0
app/transferring
Normal file
65
app/utils.py
Executable file
65
app/utils.py
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
import os
|
||||||
|
import fitz
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from database import AsyncSessionLocal
|
||||||
|
from models import Fatura, LogProcessamento
|
||||||
|
from calculos import calcular_campos_dinamicos
|
||||||
|
from layouts.equatorial_go import extrair_dados as extrair_dados_equatorial
|
||||||
|
from sqlalchemy.future import select
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def extrair_dados_pdf(caminho_pdf):
|
||||||
|
try:
|
||||||
|
with fitz.open(caminho_pdf) as doc:
|
||||||
|
texto_final = ""
|
||||||
|
for page in doc:
|
||||||
|
blocos = page.get_text("blocks")
|
||||||
|
blocos.sort(key=lambda b: (b[1], b[0]))
|
||||||
|
for b in blocos:
|
||||||
|
texto_final += b[4] + "\n"
|
||||||
|
|
||||||
|
if not texto_final.strip():
|
||||||
|
raise ValueError("PDF não contém texto legível")
|
||||||
|
|
||||||
|
dados_extraidos = extrair_dados_equatorial(texto_final)
|
||||||
|
return dados_extraidos
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Erro ao processar PDF: {str(e)}")
|
||||||
|
|
||||||
|
async def nota_ja_existente(nota_fiscal, uc):
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
result = await session.execute(
|
||||||
|
select(Fatura).filter_by(nota_fiscal=nota_fiscal, unidade_consumidora=uc)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none() is not None
|
||||||
|
|
||||||
|
async def adicionar_fatura(dados, caminho_pdf):
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
try:
|
||||||
|
dados_calculados = await calcular_campos_dinamicos(dados, session)
|
||||||
|
fatura = Fatura(**dados_calculados)
|
||||||
|
fatura.arquivo = os.path.basename(caminho_pdf)
|
||||||
|
fatura.link_arquivo = os.path.abspath(caminho_pdf)
|
||||||
|
fatura.data_processamento = datetime.now()
|
||||||
|
|
||||||
|
session.add(fatura)
|
||||||
|
|
||||||
|
log = LogProcessamento(
|
||||||
|
status="PROCESSAMENTO",
|
||||||
|
mensagem=f"Fatura adicionada com sucesso: {fatura.nota_fiscal} - {fatura.nome}",
|
||||||
|
nome_arquivo=os.path.basename(caminho_pdf)
|
||||||
|
)
|
||||||
|
session.add(log)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info(log.mensagem)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erro ao adicionar fatura no banco: {e}")
|
||||||
|
await session.rollback()
|
||||||
|
raise
|
||||||
0
app/writing
Normal file
0
app/writing
Normal file
BIN
app/~$planilha_faturas.xlsx
Executable file
BIN
app/~$planilha_faturas.xlsx
Executable file
Binary file not shown.
Reference in New Issue
Block a user