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
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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
|
||||
|
||||
95
app/main.py
95
app/main.py
@@ -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
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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__)
|
||||
|
||||
Reference in New Issue
Block a user