Atualiza exibição do tempo por processo e garante consistência da estrutura em app/
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-07-28 22:31:31 -03:00
parent d863d7f9e2
commit f3c2b08a69
82 changed files with 183 additions and 7786 deletions

View File

@@ -2,7 +2,7 @@
from decimal import Decimal
from datetime import datetime
from models import ParametrosFormula, SelicMensal
from app.models import ParametrosFormula, SelicMensal
from sqlalchemy.future import select
from sqlalchemy.ext.asyncio import AsyncSession
import re

View File

@@ -8,12 +8,9 @@ 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 (
from app.database import AsyncSessionLocal
from app.models import Fatura
from app.processor import (
fila_processamento,
processar_em_lote,
status_arquivos,
@@ -21,8 +18,8 @@ from processor import (
)
app = FastAPI()
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="app/templates")
app.mount("/static", StaticFiles(directory="app/static"), name="static")
UPLOAD_DIR = "uploads/temp"
os.makedirs(UPLOAD_DIR, exist_ok=True)
@@ -57,20 +54,8 @@ 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
)
})
def parametros_page(request: Request):
return templates.TemplateResponse("parametros.html", {"request": request})
@app.post("/upload-files")
async def upload_files(files: list[UploadFile] = File(...)):
@@ -93,14 +78,24 @@ async def process_queue():
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
})
if isinstance(status, dict):
files.append({
"nome": nome,
"status": status.get("status", "Erro"),
"mensagem": status.get("mensagem", "---"),
"tempo": status.get("tempo", "---") # ✅ AQUI
})
else:
files.append({
"nome": nome,
"status": status,
"mensagem": "---" if status == "Concluído" else status,
"tempo": "---" # ✅ AQUI também
})
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()
@@ -149,49 +144,3 @@ async def export_excel():
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
})

View File

@@ -3,7 +3,7 @@ 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
from app.database import Base
class ParametrosFormula(Base):
__tablename__ = 'parametros_formula'
@@ -13,11 +13,6 @@ class ParametrosFormula(Base):
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"

View File

@@ -3,13 +3,14 @@ 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
from app.utils import extrair_dados_pdf
from app.database import AsyncSessionLocal
from app.models import Fatura, LogProcessamento
import time
logger = logging.getLogger(__name__)
UPLOADS_DIR = os.path.join(os.getcwd(), "uploads")
UPLOADS_DIR = os.path.join("app", "uploads")
TEMP_DIR = os.path.join(UPLOADS_DIR, "temp")
fila_processamento = asyncio.Queue()
@@ -35,56 +36,92 @@ def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
return caminho_pdf_temp
async def process_single_file(caminho_pdf_temp: str, nome_original: str):
inicio = time.perf_counter()
async with AsyncSessionLocal() as session:
try:
dados = extrair_dados_pdf(caminho_pdf_temp)
dados['arquivo_pdf'] = nome_original
# Verifica se a fatura já existe
existente_result = await session.execute(
select(Fatura).filter_by(nota_fiscal=dados['nota_fiscal'], unidade_consumidora=dados['unidade_consumidora'])
select(Fatura).filter_by(
nota_fiscal=dados['nota_fiscal'],
unidade_consumidora=dados['unidade_consumidora']
)
)
if existente_result.scalar_one_or_none():
duracao = round(time.perf_counter() - inicio, 2)
remover_arquivo_temp(caminho_pdf_temp)
return {"status": "Duplicado", "dados": dados}
return {
"status": "Duplicado",
"dados": dados,
"tempo": f"{duracao}s"
}
# Salva arquivo final
caminho_final = salvar_em_uploads(caminho_pdf_temp, nome_original, dados['nota_fiscal'])
dados['link_arquivo'] = caminho_final
# Salva fatura
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}
duracao = round(time.perf_counter() - inicio, 2)
return {
"status": "Concluído",
"dados": dados,
"tempo": f"{duracao}s"
}
except Exception as e:
erro_str = traceback.format_exc()
duracao = round(time.perf_counter() - inicio, 2)
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)}
print(f"\n📄 ERRO no arquivo: {nome_original}")
print(f"⏱ Tempo até erro: {duracao}s")
print(f"❌ Erro detalhado:\n{erro_str}")
return {
"status": "Erro",
"mensagem": str(e),
"tempo": f"{duracao}s",
"trace": erro_str
}
async def processar_em_lote():
import traceback # para exibir erros
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)
try:
resultado = await process_single_file(item['caminho_pdf'], item['nome_original'])
status_arquivos[item['nome_original']] = {
"status": resultado.get("status"),
"mensagem": resultado.get("mensagem", ""),
"tempo": resultado.get("tempo", "---")
}
resultados.append(status_arquivos[item['nome_original']])
except Exception as e:
status_arquivos[item['nome_original']] = {
"status": "Erro",
"mensagem": str(e),
"tempo": "---"
}
resultados.append({
"nome": item['nome_original'],
"status": "Erro",
"mensagem": str(e)
})
print(f"Erro ao processar {item['nome_original']}: {e}")
print(traceback.format_exc())
return resultados
def limpar_arquivos_processados():

View File

@@ -1,6 +1,6 @@
# startup.py
import logging
from routes.selic import atualizar_selic_com_base_na_competencia
from app.routes.selic import atualizar_selic_com_base_na_competencia
async def executar_rotinas_iniciais(db):
try:

View File

@@ -25,6 +25,7 @@
<th>Arquivo</th>
<th>Status</th>
<th>Mensagem</th>
<th>Tempo</th>
</tr>
</thead>
<tbody id="file-table">
@@ -42,18 +43,31 @@
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>';
}
function renderTable(statusList = []) {
const rows = statusList.length ? statusList : arquivos.map(file => ({
nome: file.name,
status: 'Aguardando',
mensagem: ''
}));
fileTable.innerHTML = rows.length
? rows.map(file => {
const status = file.status || 'Aguardando';
const statusClass = status === 'Concluído' ? 'status-ok'
: status === 'Erro' ? 'status-error'
: status === 'Aguardando' ? 'status-warn'
: 'status-processing';
return `
<tr>
<td>${file.nome}</td>
<td class="${statusClass}">${status}</td>
<td>${file.mensagem || '---'}</td>
<td>${file.tempo || '---'}</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.");
@@ -65,11 +79,17 @@
try {
await fetch("/upload-files", { method: "POST", body: formData });
await fetch("/process-queue", { method: "POST" });
arquivos = [];
const response = await fetch("/process-queue", { method: "POST" });
if (!response.ok) {
const msg = await response.text();
throw new Error(`Erro no processamento: ${msg}`);
}
arquivos = []; // <- só limpa se o processamento for bem-sucedido
statusInterval = setInterval(updateStatus, 1000);
} catch (err) {
alert("Erro ao processar faturas.");
alert("Erro ao processar faturas:\n" + err.message);
} finally {
setTimeout(() => {
btn.innerText = "Processar Faturas";
@@ -78,6 +98,7 @@
}
}
async function updateStatus() {
const res = await fetch("/get-status");
const data = await res.json();
@@ -91,6 +112,7 @@
function limpar() {
fetch("/clear-all", { method: "POST" });
arquivos = [];
document.getElementById("file-input").value = null;
renderTable();
}
@@ -102,7 +124,7 @@
window.open('/generate-report', '_blank');
}
const dropZone = document.getElementById('upload-box');
const dropZone = document.body;
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
@@ -117,6 +139,23 @@
});
window.addEventListener('DOMContentLoaded', updateStatus);
// Adiciona destaque visual à caixa ao arrastar arquivos na tela toda
window.addEventListener('dragover', (e) => {
e.preventDefault();
document.getElementById('upload-box').classList.add('dragover');
});
window.addEventListener('dragleave', () => {
document.getElementById('upload-box').classList.remove('dragover');
});
window.addEventListener('drop', (e) => {
e.preventDefault();
document.getElementById('upload-box').classList.remove('dragover');
handleFiles(e.dataTransfer.files);
});
</script>
<style>
@@ -179,6 +218,12 @@
.status-ok { color: #198754; }
.status-error { color: #dc3545; }
.status-warn { color: #ffc107; }
.status-processing {
color: #0d6efd; /* azul */
font-weight: bold;
}
</style>
{% endblock %}

View File

@@ -3,10 +3,10 @@ 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 app.database import AsyncSessionLocal
from app.models import Fatura, LogProcessamento
from app.calculos import calcular_campos_dinamicos
from app.layouts.equatorial_go import extrair_dados as extrair_dados_equatorial
from sqlalchemy.future import select
logger = logging.getLogger(__name__)