Compare commits
1 Commits
a024125982
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| 867be98943 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,4 +3,3 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
.venv/
|
.venv/
|
||||||
.vscode/
|
.vscode/
|
||||||
uploads/
|
|
||||||
BIN
.main.py.swn
Normal file
BIN
.main.py.swn
Normal file
Binary file not shown.
BIN
.main.py.swo
Normal file
BIN
.main.py.swo
Normal file
Binary file not shown.
BIN
.main.py.swp
Normal file
BIN
.main.py.swp
Normal file
Binary file not shown.
@@ -19,4 +19,4 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "5000"]
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"]
|
||||||
0
[internal]
Normal file
0
[internal]
Normal file
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.models import ParametrosFormula, SelicMensal
|
from models import ParametrosFormula, SelicMensal
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
import re
|
import re
|
||||||
|
|||||||
111
app/main.py
111
app/main.py
@@ -4,26 +4,25 @@ from fastapi.templating import Jinja2Templates
|
|||||||
from fastapi.responses import HTMLResponse, JSONResponse
|
from fastapi.responses import HTMLResponse, JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
import os, shutil
|
import os, shutil
|
||||||
from sqlalchemy import text
|
|
||||||
from fastapi import Depends
|
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from app.models import ParametrosFormula
|
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from app.database import AsyncSessionLocal
|
from database import AsyncSessionLocal
|
||||||
from app.models import Fatura
|
from models import Fatura
|
||||||
from app.processor import (
|
from models import ParametrosFormula
|
||||||
|
from fastapi import Form
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from processor import (
|
||||||
fila_processamento,
|
fila_processamento,
|
||||||
processar_em_lote,
|
processar_em_lote,
|
||||||
status_arquivos,
|
status_arquivos,
|
||||||
limpar_arquivos_processados
|
limpar_arquivos_processados
|
||||||
)
|
)
|
||||||
from app.parametros import router as parametros_router
|
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
templates = Jinja2Templates(directory="app/templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
UPLOAD_DIR = "uploads/temp"
|
UPLOAD_DIR = "uploads/temp"
|
||||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||||
@@ -51,11 +50,7 @@ def dashboard(request: Request):
|
|||||||
|
|
||||||
@app.get("/upload", response_class=HTMLResponse)
|
@app.get("/upload", response_class=HTMLResponse)
|
||||||
def upload_page(request: Request):
|
def upload_page(request: Request):
|
||||||
app_env = os.getenv("APP_ENV", "dev") # Captura variável de ambiente
|
return templates.TemplateResponse("upload.html", {"request": request})
|
||||||
return templates.TemplateResponse("upload.html", {
|
|
||||||
"request": request,
|
|
||||||
"app_env": app_env # Passa para o template
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.get("/relatorios", response_class=HTMLResponse)
|
@app.get("/relatorios", response_class=HTMLResponse)
|
||||||
def relatorios_page(request: Request):
|
def relatorios_page(request: Request):
|
||||||
@@ -64,11 +59,17 @@ def relatorios_page(request: Request):
|
|||||||
@app.get("/parametros", response_class=HTMLResponse)
|
@app.get("/parametros", response_class=HTMLResponse)
|
||||||
async def parametros_page(request: Request):
|
async def parametros_page(request: Request):
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
result = await session.execute(select(ParametrosFormula))
|
result = await session.execute(select(ParametrosFormula).limit(1))
|
||||||
parametros = result.scalars().first()
|
parametros = result.scalar_one_or_none()
|
||||||
|
|
||||||
return templates.TemplateResponse("parametros.html", {
|
return templates.TemplateResponse("parametros.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"parametros": parametros or {}
|
"parametros": parametros or SimpleNamespace(
|
||||||
|
aliquota_icms=None,
|
||||||
|
incluir_icms=True,
|
||||||
|
incluir_pis=True,
|
||||||
|
incluir_cofins=True
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.post("/upload-files")
|
@app.post("/upload-files")
|
||||||
@@ -92,27 +93,14 @@ async def process_queue():
|
|||||||
async def get_status():
|
async def get_status():
|
||||||
files = []
|
files = []
|
||||||
for nome, status in status_arquivos.items():
|
for nome, status in status_arquivos.items():
|
||||||
if isinstance(status, dict):
|
|
||||||
files.append({
|
|
||||||
"nome": nome,
|
|
||||||
"status": status.get("status", "Erro"),
|
|
||||||
"mensagem": status.get("mensagem", "---"),
|
|
||||||
"tempo": status.get("tempo", "---"),
|
|
||||||
"tamanho": f"{status.get('tamanho', 0)} KB",
|
|
||||||
"data": status.get("data", "")
|
|
||||||
})
|
|
||||||
|
|
||||||
else:
|
|
||||||
files.append({
|
files.append({
|
||||||
"nome": nome,
|
"nome": nome,
|
||||||
"status": status,
|
"status": status,
|
||||||
"mensagem": "---" if status == "Concluído" else status,
|
"mensagem": "---" if status == "Concluído" else status
|
||||||
"tempo": "---" # ✅ AQUI também
|
|
||||||
})
|
})
|
||||||
is_processing = not fila_processamento.empty()
|
is_processing = not fila_processamento.empty()
|
||||||
return JSONResponse(content={"is_processing": is_processing, "files": files})
|
return JSONResponse(content={"is_processing": is_processing, "files": files})
|
||||||
|
|
||||||
|
|
||||||
@app.post("/clear-all")
|
@app.post("/clear-all")
|
||||||
async def clear_all():
|
async def clear_all():
|
||||||
limpar_arquivos_processados()
|
limpar_arquivos_processados()
|
||||||
@@ -162,27 +150,48 @@ async def export_excel():
|
|||||||
headers={"Content-Disposition": "attachment; filename=relatorio_faturas.xlsx"}
|
headers={"Content-Disposition": "attachment; filename=relatorio_faturas.xlsx"}
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.parametros import router as parametros_router
|
@app.post("/parametros", response_class=HTMLResponse)
|
||||||
app.include_router(parametros_router)
|
async def salvar_parametros(
|
||||||
|
request: Request,
|
||||||
def is_homolog():
|
aliquota_icms: float = Form(...),
|
||||||
return os.getenv("APP_ENV", "dev") == "homolog"
|
formula_pis: str = Form(...),
|
||||||
|
formula_cofins: str = Form(...)
|
||||||
@app.post("/limpar-faturas")
|
):
|
||||||
async def limpar_faturas():
|
|
||||||
app_env = os.getenv("APP_ENV", "dev")
|
|
||||||
if app_env not in ["homolog", "dev", "local"]:
|
|
||||||
return JSONResponse(status_code=403, content={"message": "Operação não permitida neste ambiente."})
|
|
||||||
|
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
print("🧪 Limpando faturas do banco...")
|
result = await session.execute(select(ParametrosFormula).limit(1))
|
||||||
await session.execute(text("DELETE FROM faturas.faturas"))
|
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()
|
await session.commit()
|
||||||
|
|
||||||
upload_path = os.path.join("app", "uploads")
|
parametros = SimpleNamespace(
|
||||||
for nome in os.listdir(upload_path):
|
aliquota_icms=aliquota_icms,
|
||||||
caminho = os.path.join(upload_path, nome)
|
incluir_icms=1,
|
||||||
if os.path.isfile(caminho):
|
incluir_pis=1,
|
||||||
os.remove(caminho)
|
incluir_cofins=1,
|
||||||
|
formula_pis=formula_pis,
|
||||||
|
formula_cofins=formula_cofins
|
||||||
|
)
|
||||||
|
|
||||||
return {"message": "Faturas e arquivos apagados com sucesso."}
|
return templates.TemplateResponse("parametros.html", {
|
||||||
|
"request": request,
|
||||||
|
"parametros": parametros,
|
||||||
|
"mensagem": mensagem
|
||||||
|
})
|
||||||
@@ -3,20 +3,19 @@ from sqlalchemy import Column, String, Integer, Float, DateTime, Text
|
|||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.database import Base
|
from database import Base
|
||||||
from sqlalchemy import Boolean
|
|
||||||
|
|
||||||
|
|
||||||
class ParametrosFormula(Base):
|
class ParametrosFormula(Base):
|
||||||
__tablename__ = "parametros_formula"
|
__tablename__ = 'parametros_formula'
|
||||||
__table_args__ = {"schema": "faturas"}
|
__table_args__ = {'schema': 'faturas', 'extend_existing': True}
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
tipo = Column(String(20))
|
nome = Column(String)
|
||||||
formula = Column(Text)
|
formula = Column(Text)
|
||||||
ativo = Column(Boolean)
|
|
||||||
|
# Novos campos
|
||||||
aliquota_icms = Column(Float)
|
aliquota_icms = Column(Float)
|
||||||
incluir_icms = Column(Integer)
|
incluir_icms = Column(Integer) # Use Boolean se preferir
|
||||||
incluir_pis = Column(Integer)
|
incluir_pis = Column(Integer)
|
||||||
incluir_cofins = Column(Integer)
|
incluir_cofins = Column(Integer)
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,13 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import asyncio
|
import asyncio
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
from app.utils import extrair_dados_pdf
|
from utils import extrair_dados_pdf
|
||||||
from app.database import AsyncSessionLocal
|
from database import AsyncSessionLocal
|
||||||
from app.models import Fatura, LogProcessamento
|
from models import Fatura, LogProcessamento
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
UPLOADS_DIR = os.path.join("app", "uploads")
|
UPLOADS_DIR = os.path.join(os.getcwd(), "uploads")
|
||||||
TEMP_DIR = os.path.join(UPLOADS_DIR, "temp")
|
TEMP_DIR = os.path.join(UPLOADS_DIR, "temp")
|
||||||
|
|
||||||
fila_processamento = asyncio.Queue()
|
fila_processamento = asyncio.Queue()
|
||||||
@@ -30,7 +26,7 @@ def remover_arquivo_temp(caminho_pdf):
|
|||||||
def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
|
def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
|
||||||
try:
|
try:
|
||||||
extensao = os.path.splitext(nome_original)[1].lower()
|
extensao = os.path.splitext(nome_original)[1].lower()
|
||||||
nome_destino = f"{nota_fiscal}_{uuid.uuid4().hex[:6]}{extensao}"
|
nome_destino = f"{nota_fiscal}{extensao}"
|
||||||
destino_final = os.path.join(UPLOADS_DIR, nome_destino)
|
destino_final = os.path.join(UPLOADS_DIR, nome_destino)
|
||||||
shutil.copy2(caminho_pdf_temp, destino_final)
|
shutil.copy2(caminho_pdf_temp, destino_final)
|
||||||
return destino_final
|
return destino_final
|
||||||
@@ -39,95 +35,56 @@ def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
|
|||||||
return caminho_pdf_temp
|
return caminho_pdf_temp
|
||||||
|
|
||||||
async def process_single_file(caminho_pdf_temp: str, nome_original: str):
|
async def process_single_file(caminho_pdf_temp: str, nome_original: str):
|
||||||
inicio = time.perf_counter()
|
|
||||||
|
|
||||||
async with AsyncSessionLocal() as session:
|
async with AsyncSessionLocal() as session:
|
||||||
try:
|
try:
|
||||||
dados = extrair_dados_pdf(caminho_pdf_temp)
|
dados = extrair_dados_pdf(caminho_pdf_temp)
|
||||||
dados['arquivo_pdf'] = nome_original
|
dados['arquivo_pdf'] = nome_original
|
||||||
|
|
||||||
# Verifica se a fatura já existe
|
|
||||||
existente_result = await session.execute(
|
existente_result = await session.execute(
|
||||||
select(Fatura).filter_by(
|
select(Fatura).filter_by(nota_fiscal=dados['nota_fiscal'], unidade_consumidora=dados['unidade_consumidora'])
|
||||||
nota_fiscal=dados['nota_fiscal'],
|
|
||||||
unidade_consumidora=dados['unidade_consumidora']
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if existente_result.scalar_one_or_none():
|
if existente_result.scalar_one_or_none():
|
||||||
duracao = round(time.perf_counter() - inicio, 2)
|
|
||||||
remover_arquivo_temp(caminho_pdf_temp)
|
remover_arquivo_temp(caminho_pdf_temp)
|
||||||
return {
|
return {"status": "Duplicado", "dados": dados}
|
||||||
"status": "Duplicado",
|
|
||||||
"dados": dados,
|
|
||||||
"tempo": f"{duracao}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Salva arquivo final
|
|
||||||
caminho_final = salvar_em_uploads(caminho_pdf_temp, nome_original, dados['nota_fiscal'])
|
caminho_final = salvar_em_uploads(caminho_pdf_temp, nome_original, dados['nota_fiscal'])
|
||||||
dados['link_arquivo'] = caminho_final
|
dados['link_arquivo'] = caminho_final
|
||||||
|
|
||||||
# Salva fatura
|
|
||||||
fatura = Fatura(**dados)
|
fatura = Fatura(**dados)
|
||||||
session.add(fatura)
|
session.add(fatura)
|
||||||
|
|
||||||
|
session.add(LogProcessamento(
|
||||||
|
status="Sucesso",
|
||||||
|
mensagem="Fatura processada com sucesso",
|
||||||
|
nome_arquivo=nome_original,
|
||||||
|
acao="PROCESSAMENTO"
|
||||||
|
))
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
remover_arquivo_temp(caminho_pdf_temp)
|
remover_arquivo_temp(caminho_pdf_temp)
|
||||||
duracao = round(time.perf_counter() - inicio, 2)
|
return {"status": "Concluído", "dados": dados}
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "Concluído",
|
|
||||||
"dados": dados,
|
|
||||||
"tempo": f"{duracao}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
erro_str = traceback.format_exc()
|
|
||||||
duracao = round(time.perf_counter() - inicio, 2)
|
|
||||||
await session.rollback()
|
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)
|
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():
|
async def processar_em_lote():
|
||||||
import traceback # para exibir erros
|
|
||||||
resultados = []
|
resultados = []
|
||||||
while not fila_processamento.empty():
|
while not fila_processamento.empty():
|
||||||
item = await fila_processamento.get()
|
item = await fila_processamento.get()
|
||||||
try:
|
|
||||||
resultado = await process_single_file(item['caminho_pdf'], item['nome_original'])
|
resultado = await process_single_file(item['caminho_pdf'], item['nome_original'])
|
||||||
status_arquivos[item['nome_original']] = {
|
status_arquivos[item['nome_original']] = resultado.get("status", "Erro")
|
||||||
"status": resultado.get("status"),
|
resultados.append(resultado)
|
||||||
"mensagem": resultado.get("mensagem", ""),
|
|
||||||
"tempo": resultado.get("tempo", "---"),
|
|
||||||
"tamanho": os.path.getsize(item['caminho_pdf']) // 1024, # tamanho em KB
|
|
||||||
"data": time.strftime("%d/%m/%Y", time.localtime(os.path.getmtime(item['caminho_pdf'])))
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return resultados
|
||||||
|
|
||||||
def limpar_arquivos_processados():
|
def limpar_arquivos_processados():
|
||||||
|
|||||||
22
app/parametros.py → app/routes/parametros.py
Normal file → Executable file
22
app/parametros.py → app/routes/parametros.py
Normal file → Executable file
@@ -1,15 +1,12 @@
|
|||||||
# parametros.py
|
# parametros.py
|
||||||
from fastapi import APIRouter, Request, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.database import get_session
|
from database import get_session
|
||||||
from app.models import AliquotaUF, ParametrosFormula, SelicMensal
|
from models import AliquotaUF, ParametrosFormula, SelicMensal
|
||||||
from typing import List
|
from typing import List
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import datetime
|
import datetime
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from sqlalchemy.future import select
|
|
||||||
from app.database import AsyncSessionLocal
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -39,19 +36,6 @@ class SelicMensalSchema(BaseModel):
|
|||||||
|
|
||||||
# === Rotas ===
|
# === Rotas ===
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
templates = Jinja2Templates(directory="app/templates")
|
|
||||||
|
|
||||||
@router.get("/parametros")
|
|
||||||
async def parametros_page(request: Request):
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
result = await session.execute(select(ParametrosFormula).where(ParametrosFormula.ativo == True))
|
|
||||||
parametros = result.scalars().first()
|
|
||||||
return templates.TemplateResponse("parametros.html", {
|
|
||||||
"request": request,
|
|
||||||
"parametros": parametros or {}
|
|
||||||
})
|
|
||||||
|
|
||||||
@router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema])
|
@router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema])
|
||||||
def listar_aliquotas(db: AsyncSession = Depends(get_session)):
|
def listar_aliquotas(db: AsyncSession = Depends(get_session)):
|
||||||
return db.query(AliquotaUF).all()
|
return db.query(AliquotaUF).all()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# startup.py
|
# startup.py
|
||||||
import logging
|
import logging
|
||||||
from app.routes.selic import atualizar_selic_com_base_na_competencia
|
from routes.selic import atualizar_selic_com_base_na_competencia
|
||||||
|
|
||||||
async def executar_rotinas_iniciais(db):
|
async def executar_rotinas_iniciais(db):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,206 +1,32 @@
|
|||||||
{% extends "index.html" %}
|
{% extends "index.html" %}
|
||||||
{% block title %}Parâmetros de Cálculo{% endblock %}
|
{% block title %}Parâmetros de Cálculo{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<h1>⚙️ Parâmetros</h1>
|
||||||
|
|
||||||
<h1 style="font-size: 1.6rem; margin-bottom: 1rem; display:flex; align-items:center; gap:0.5rem;">
|
<form method="post">
|
||||||
⚙️ Parâmetros de Cálculo
|
<label for="tipo">Tipo:</label><br>
|
||||||
</h1>
|
<input type="text" name="tipo" id="tipo" value="{{ parametros.tipo or '' }}" required/><br><br>
|
||||||
|
|
||||||
<div class="tabs">
|
<label for="formula">Fórmula:</label><br>
|
||||||
<button class="tab active" onclick="switchTab('formulas')">📄 Fórmulas</button>
|
<input type="text" name="formula" id="formula" value="{{ parametros.formula or '' }}" required/><br><br>
|
||||||
<button class="tab" onclick="switchTab('selic')">📊 Gestão SELIC</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ABA FÓRMULAS -->
|
<label for="aliquota_icms">Alíquota de ICMS (%):</label><br>
|
||||||
<div id="formulas" class="tab-content active">
|
<input type="number" step="0.01" name="aliquota_icms" id="aliquota_icms" value="{{ parametros.aliquota_icms or '' }}" /><br><br>
|
||||||
<form method="post" class="formulario-box">
|
|
||||||
<div class="grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="tipo">Tipo:</label>
|
|
||||||
<input type="text" name="tipo" id="tipo" value="{{ parametros.tipo or '' }}" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="aliquota_icms">Alíquota de ICMS (%):</label>
|
|
||||||
<input type="number" step="0.01" name="aliquota_icms" id="aliquota_icms" value="{{ parametros.aliquota_icms or '' }}" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<label for="incluir_icms">Incluir ICMS:</label><br>
|
||||||
<label for="formula">Fórmula:</label>
|
<input type="checkbox" name="incluir_icms" id="incluir_icms" value="1" {% if parametros.incluir_icms %}checked{% endif %}><br><br>
|
||||||
<div class="editor-box">
|
|
||||||
<select onchange="inserirCampo(this)">
|
|
||||||
<option value="">Inserir campo...</option>
|
|
||||||
<option value="pis_base">pis_base</option>
|
|
||||||
<option value="cofins_base">cofins_base</option>
|
|
||||||
<option value="icms_valor">icms_valor</option>
|
|
||||||
<option value="pis_aliq">pis_aliq</option>
|
|
||||||
<option value="cofins_aliq">cofins_aliq</option>
|
|
||||||
</select>
|
|
||||||
<textarea name="formula" id="formula" rows="3" required>{{ parametros.formula or '' }}</textarea>
|
|
||||||
<div class="actions-inline">
|
|
||||||
<button type="button" class="btn btn-secondary" onclick="testarFormula()">🧪 Testar Fórmula</button>
|
|
||||||
<span class="exemplo">Ex: (pis_base - (pis_base - icms_valor)) * pis_aliq</span>
|
|
||||||
</div>
|
|
||||||
<div id="resultado-teste" class="mensagem-info" style="display:none;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group check-group">
|
<label for="ativo">Ativo:</label><br>
|
||||||
<label><input type="checkbox" name="incluir_icms" value="1" {% if parametros.incluir_icms %}checked{% endif %}> Incluir ICMS</label>
|
<input type="checkbox" name="ativo" id="ativo" value="1" {% if parametros.ativo %}checked{% endif %}><br><br>
|
||||||
<label><input type="checkbox" name="incluir_pis" value="1" {% if parametros.incluir_pis %}checked{% endif %}> Incluir PIS</label>
|
|
||||||
<label><input type="checkbox" name="incluir_cofins" value="1" {% if parametros.incluir_cofins %}checked{% endif %}> Incluir COFINS</label>
|
|
||||||
<label><input type="checkbox" name="ativo" value="1" {% if parametros.ativo %}checked{% endif %}> Ativo</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">💾 Salvar Parâmetro</button>
|
<button type="submit" style="padding: 10px 20px; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
Salvar Parâmetros
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h3 style="margin-top: 2rem;">📋 Fórmulas Salvas</h3>
|
{% if mensagem %}
|
||||||
<div class="card-list">
|
<div style="margin-top: 20px; background: #e0f7fa; padding: 10px; border-left: 4px solid #2563eb;">
|
||||||
{% for param in lista_parametros %}
|
{{ mensagem }}
|
||||||
<div class="param-card">
|
|
||||||
<h4>{{ param.tipo }}</h4>
|
|
||||||
<code>{{ param.formula }}</code>
|
|
||||||
<div class="actions">
|
|
||||||
<a href="?editar={{ param.id }}" class="btn btn-sm">✏️ Editar</a>
|
|
||||||
<a href="?testar={{ param.id }}" class="btn btn-sm btn-secondary">🧪 Testar</a>
|
|
||||||
<a href="/parametros/delete/{{ param.id }}" class="btn btn-sm btn-danger">🗑️ Excluir</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
{% else %}<p style="color:gray;">Nenhuma fórmula cadastrada.</p>{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ABA SELIC -->
|
|
||||||
<div id="selic" class="tab-content" style="display:none;">
|
|
||||||
<div class="formulario-box">
|
|
||||||
<h3>📈 Gestão da SELIC</h3>
|
|
||||||
<p>Utilize o botão abaixo para importar os fatores SELIC automaticamente a partir da API do Banco Central.</p>
|
|
||||||
<form method="post" action="/parametros/selic/importar">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="data_maxima">Data máxima para cálculo da SELIC:</label>
|
|
||||||
<input type="date" id="data_maxima" name="data_maxima" value="{{ data_maxima or '' }}" />
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">⬇️ Atualizar Fatores SELIC</button>
|
|
||||||
</form>
|
|
||||||
<div class="mensagem-info" style="margin-top:1rem;">Última data coletada da SELIC: <strong>{{ ultima_data_selic }}</strong></div>
|
|
||||||
<table class="selic-table">
|
|
||||||
<thead><tr><th>Competência</th><th>Fator</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
{% for item in selic_dados %}
|
|
||||||
<tr><td>{{ item.competencia }}</td><td>{{ item.fator }}</td></tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ESTILOS -->
|
|
||||||
<style>
|
|
||||||
.tabs { display: flex; gap: 1rem; margin-bottom: 1rem; }
|
|
||||||
.tab { background: none; border: none; font-weight: bold; cursor: pointer; padding: 0.5rem 1rem; border-bottom: 2px solid transparent; }
|
|
||||||
.tab.active { border-bottom: 2px solid #2563eb; color: #2563eb; }
|
|
||||||
.tab-content { display: none; }
|
|
||||||
.tab-content.active { display: block; }
|
|
||||||
|
|
||||||
.formulario-box {
|
|
||||||
background: #fff;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.05);
|
|
||||||
max-width: 850px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group { margin-bottom: 1rem; }
|
|
||||||
.form-group label { display: block; font-weight: bold; margin-bottom: 0.5rem; }
|
|
||||||
.form-group input[type="text"],
|
|
||||||
.form-group input[type="number"],
|
|
||||||
.form-group input[type="date"],
|
|
||||||
.form-group textarea { width: 100%; padding: 0.6rem; border-radius: 6px; border: 1px solid #ccc; }
|
|
||||||
|
|
||||||
.form-group.check-group { display: flex; gap: 1rem; flex-wrap: wrap; }
|
|
||||||
.check-group label { display: flex; align-items: center; gap: 0.5rem; }
|
|
||||||
|
|
||||||
.editor-box select { margin-bottom: 0.5rem; }
|
|
||||||
.editor-box textarea { font-family: monospace; }
|
|
||||||
.actions-inline { display: flex; gap: 1rem; align-items: center; margin-top: 0.5rem; }
|
|
||||||
|
|
||||||
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; }
|
|
||||||
.btn-primary { background-color: #2563eb; color: white; }
|
|
||||||
.btn-secondary { background-color: #6c757d; color: white; }
|
|
||||||
.btn-danger { background-color: #dc3545; color: white; }
|
|
||||||
.btn-sm { font-size: 0.85rem; padding: 0.3rem 0.6rem; }
|
|
||||||
|
|
||||||
.mensagem-info {
|
|
||||||
background: #e0f7fa;
|
|
||||||
padding: 1rem;
|
|
||||||
border-left: 4px solid #2563eb;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #007b83;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
|
||||||
|
|
||||||
.card-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
.param-card {
|
|
||||||
background: #f9f9f9;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid #2563eb;
|
|
||||||
}
|
|
||||||
.param-card code { display: block; margin: 0.5rem 0; color: #333; }
|
|
||||||
.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
||||||
|
|
||||||
.selic-table { width: 100%; margin-top: 1rem; border-collapse: collapse; }
|
|
||||||
.selic-table th, .selic-table td { padding: 0.6rem; border-bottom: 1px solid #ccc; }
|
|
||||||
.selic-table th { text-align: left; background: #f1f1f1; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function switchTab(tabId) {
|
|
||||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
||||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
||||||
document.getElementById(tabId).classList.add('active');
|
|
||||||
event.target.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
function inserirCampo(select) {
|
|
||||||
const campo = select.value;
|
|
||||||
if (campo) {
|
|
||||||
const formula = document.getElementById("formula");
|
|
||||||
const start = formula.selectionStart;
|
|
||||||
const end = formula.selectionEnd;
|
|
||||||
formula.value = formula.value.slice(0, start) + campo + formula.value.slice(end);
|
|
||||||
formula.focus();
|
|
||||||
formula.setSelectionRange(start + campo.length, start + campo.length);
|
|
||||||
select.selectedIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function testarFormula() {
|
|
||||||
const formula = document.getElementById("formula").value;
|
|
||||||
const vars = {
|
|
||||||
pis_base: 84.38,
|
|
||||||
cofins_base: 84.38,
|
|
||||||
icms_valor: 24.47,
|
|
||||||
pis_aliq: 0.012872,
|
|
||||||
cofins_aliq: 0.059287
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const resultado = eval(formula.replace(/\b(\w+)\b/g, match => vars.hasOwnProperty(match) ? vars[match] : match));
|
|
||||||
document.getElementById("resultado-teste").innerText = "Resultado: R$ " + resultado.toFixed(5);
|
|
||||||
document.getElementById("resultado-teste").style.display = "block";
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById("resultado-teste").innerText = "Erro: " + e.message;
|
|
||||||
document.getElementById("resultado-teste").style.display = "block";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -8,183 +8,74 @@
|
|||||||
<h3>Arraste faturas em PDF aqui ou clique para selecionar</h3>
|
<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>
|
<p style="color: gray; font-size: 0.9rem;">Apenas PDFs textuais (não escaneados)</p>
|
||||||
<br />
|
<br />
|
||||||
<button class="btn btn-primary" id="btn-selecionar" onclick="document.getElementById('file-input').click()">Selecionar Arquivos</button>
|
<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;" />
|
<input type="file" id="file-input" accept=".pdf" multiple onchange="handleFiles(this.files)" style="display:none;" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button class="btn btn-primary" onclick="processar(this)">Processar Faturas</button>
|
<button class="btn btn-primary" onclick="processar(this)">Processar Faturas</button>
|
||||||
<button class="btn btn-primary pulse" onclick="limpar()" style="font-weight: bold;">🔁 Novo Processo</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="baixarPlanilha()">📅 Abrir Planilha</button>
|
||||||
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button>
|
<button class="btn btn-success" onclick="gerarRelatorio()">📊 Gerar Relatório</button>
|
||||||
{% if app_env != "producao" %}
|
|
||||||
<button class="btn btn-warning" onclick="limparFaturas()">🧹 Limpar Faturas (Teste)</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div id="overlay-bloqueio" class="overlay-bloqueio hidden">
|
|
||||||
⏳ Tabela bloqueada até finalizar o processo
|
|
||||||
<div id="barra-progresso" class="barra-processamento"></div>
|
|
||||||
</div>
|
|
||||||
<div id="tabela-wrapper" class="tabela-wrapper"></div>
|
|
||||||
</div>
|
</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>
|
<script>
|
||||||
let arquivos = [];
|
let arquivos = [];
|
||||||
let statusInterval = null;
|
let statusInterval = null;
|
||||||
let processado = false;
|
|
||||||
let processamentoFinalizado = false;
|
|
||||||
|
|
||||||
const fileTable = document.getElementById('file-table');
|
const fileTable = document.getElementById('file-table');
|
||||||
|
|
||||||
function handleFiles(files) {
|
function handleFiles(files) {
|
||||||
if (processado) {
|
|
||||||
document.getElementById("feedback-sucesso").innerText = "";
|
|
||||||
document.getElementById("feedback-erro").innerText = "⚠️ Conclua ou inicie um novo processo antes de adicionar mais arquivos.";
|
|
||||||
document.getElementById("feedback-duplicado").innerText = "";
|
|
||||||
document.getElementById("upload-feedback").classList.remove("hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
arquivos = [...arquivos, ...files];
|
arquivos = [...arquivos, ...files];
|
||||||
renderTable();
|
renderTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTable(statusList = []) {
|
function renderTable(statusList = []) {
|
||||||
const grupos = ['Aguardando', 'Enviado', 'Erro', 'Duplicado'];
|
const rows = statusList.length ? statusList : arquivos.map(file => ({ nome: file.name, status: 'Aguardando', mensagem: '' }));
|
||||||
const dados = statusList.length ? statusList : arquivos.map(file => ({
|
fileTable.innerHTML = rows.length
|
||||||
nome: file.name,
|
? rows.map(file => `
|
||||||
status: 'Aguardando',
|
|
||||||
mensagem: '',
|
|
||||||
tempo: '---',
|
|
||||||
tamanho: (file.size / 1024).toFixed(1) + " KB",
|
|
||||||
data: new Date(file.lastModified).toLocaleDateString()
|
|
||||||
}));
|
|
||||||
|
|
||||||
const htmlGrupos = grupos.map(grupo => {
|
|
||||||
const rows = dados.filter(f => f.status === grupo);
|
|
||||||
if (!rows.length) return '';
|
|
||||||
|
|
||||||
const linhas = rows.map(file => `
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>${file.nome}<br><small>${file.tamanho} • ${file.data}</small></td>
|
<td>${file.nome}</td>
|
||||||
<td class="${grupo === 'Concluído' ? 'status-ok' :
|
<td class="${file.status === 'Concluido' ? 'status-ok' : file.status === 'Erro' ? 'status-error' : 'status-warn'}">${file.status}</td>
|
||||||
grupo === 'Erro' ? 'status-error' :
|
<td>${file.mensagem || '---'}</td>
|
||||||
grupo === 'Aguardando' ? 'status-warn' :
|
|
||||||
'status-processing'}">
|
|
||||||
${grupo === 'Concluído' ? '✔️' :
|
|
||||||
grupo === 'Erro' ? '❌' :
|
|
||||||
grupo === 'Duplicado' ? '📄' :
|
|
||||||
'⌛'} ${file.status}
|
|
||||||
</td>
|
|
||||||
<td>${file.tempo || '---'}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('')
|
||||||
|
: '<tr><td colspan="3" style="text-align:center; color: #aaa;">Nenhum arquivo selecionado.</td></tr>';
|
||||||
return `
|
|
||||||
<details open class="grupo-status">
|
|
||||||
<summary><strong>${grupo}</strong> (${rows.length})</summary>
|
|
||||||
<table class="grupo-tabela"><tbody>${linhas}</tbody></table>
|
|
||||||
</details>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
document.getElementById("tabela-wrapper").innerHTML = htmlGrupos;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processar(btn) {
|
async function processar(btn) {
|
||||||
if (arquivos.length === 0) return alert("Nenhum arquivo selecionado.");
|
if (arquivos.length === 0) return alert("Nenhum arquivo selecionado.");
|
||||||
document.getElementById("tabela-wrapper").classList.add("bloqueada");
|
|
||||||
|
|
||||||
if (processamentoFinalizado) {
|
|
||||||
showPopup("⚠️ Conclua ou inicie um novo processo antes de processar novamente.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerText = "⏳ Enviando arquivos...";
|
btn.innerText = "⏳ Processando...";
|
||||||
|
|
||||||
const statusList = [];
|
|
||||||
const total = arquivos.length;
|
|
||||||
|
|
||||||
document.getElementById("overlay-bloqueio").classList.remove("hidden");
|
|
||||||
document.getElementById("barra-progresso").style.width = "0%";
|
|
||||||
|
|
||||||
for (let i = 0; i < total; i++) {
|
|
||||||
const file = arquivos[i];
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("files", file);
|
arquivos.forEach(file => formData.append("files", file));
|
||||||
|
|
||||||
// Atualiza status visual antes do envio
|
|
||||||
statusList.push({
|
|
||||||
nome: file.name,
|
|
||||||
status: "Enviando...",
|
|
||||||
mensagem: `(${i + 1}/${total})`,
|
|
||||||
tempo: "---"
|
|
||||||
});
|
|
||||||
renderTable(statusList);
|
|
||||||
|
|
||||||
const start = performance.now();
|
|
||||||
try {
|
try {
|
||||||
await fetch("/upload-files", { method: "POST", body: formData });
|
await fetch("/upload-files", { method: "POST", body: formData });
|
||||||
let progresso = Math.round(((i + 1) / total) * 100);
|
await fetch("/process-queue", { method: "POST" });
|
||||||
document.getElementById("barra-progresso").style.width = `${progresso}%`;
|
arquivos = [];
|
||||||
|
|
||||||
statusList[i].status = "Enviado";
|
|
||||||
statusList[i].tempo = `${((performance.now() - start) / 1000).toFixed(2)}s`;
|
|
||||||
} catch (err) {
|
|
||||||
statusList[i].status = "Erro";
|
|
||||||
statusList[i].mensagem = err.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTable(statusList);
|
|
||||||
|
|
||||||
// Delay de 200ms entre cada envio
|
|
||||||
await new Promise(r => setTimeout(r, 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
btn.innerText = "⏳ Iniciando processamento...";
|
|
||||||
try {
|
|
||||||
const res = await fetch("/process-queue", { method: "POST" });
|
|
||||||
if (!res.ok) throw new Error(await res.text());
|
|
||||||
|
|
||||||
statusInterval = setInterval(updateStatus, 1000);
|
statusInterval = setInterval(updateStatus, 1000);
|
||||||
|
} catch (err) {
|
||||||
// Mensagem final após pequeno delay
|
alert("Erro ao processar faturas.");
|
||||||
setTimeout(async () => {
|
|
||||||
const res = await fetch("/get-status");
|
|
||||||
const data = await res.json();
|
|
||||||
const finalStatus = data.files;
|
|
||||||
|
|
||||||
const concluidos = finalStatus.filter(f => f.status === "Concluído").length;
|
|
||||||
const erros = finalStatus.filter(f => f.status === "Erro").length;
|
|
||||||
const duplicados = finalStatus.filter(f => f.status === "Duplicado").length;
|
|
||||||
|
|
||||||
document.getElementById("feedback-sucesso").innerText = `✔️ ${concluidos} enviados com sucesso`;
|
|
||||||
document.getElementById("feedback-erro").innerText = `❌ ${erros} com erro(s)`;
|
|
||||||
document.getElementById("feedback-duplicado").innerText = `📄 ${duplicados } duplicado(s)`;
|
|
||||||
|
|
||||||
document.getElementById("tabela-wrapper").classList.remove("bloqueada");
|
|
||||||
|
|
||||||
document.getElementById("upload-feedback").classList.remove("hidden");
|
|
||||||
|
|
||||||
processamentoFinalizado = true;
|
|
||||||
|
|
||||||
}, 1000);
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById("feedback-sucesso").innerText = "";
|
|
||||||
document.getElementById("feedback-erro").innerText = `❌ Erro ao iniciar: ${e.message}`;
|
|
||||||
document.getElementById("upload-feedback").classList.remove("hidden");
|
|
||||||
document.getElementById("overlay-bloqueio").classList.add("hidden");
|
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
processado = true;
|
setTimeout(() => {
|
||||||
document.getElementById("btn-selecionar").disabled = true;
|
|
||||||
btn.innerText = "Processar Faturas";
|
btn.innerText = "Processar Faturas";
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateStatus() {
|
async function updateStatus() {
|
||||||
@@ -200,19 +91,7 @@ async function processar(btn) {
|
|||||||
function limpar() {
|
function limpar() {
|
||||||
fetch("/clear-all", { method: "POST" });
|
fetch("/clear-all", { method: "POST" });
|
||||||
arquivos = [];
|
arquivos = [];
|
||||||
processado = false;
|
|
||||||
document.getElementById("file-input").value = null;
|
|
||||||
renderTable();
|
renderTable();
|
||||||
|
|
||||||
// limpa feedback visual também
|
|
||||||
document.getElementById("upload-feedback").classList.add("hidden");
|
|
||||||
document.getElementById("feedback-sucesso").innerText = "";
|
|
||||||
document.getElementById("feedback-erro").innerText = "";
|
|
||||||
document.getElementById("feedback-duplicado").innerText = "";
|
|
||||||
document.getElementById("tabela-wrapper").classList.remove("bloqueada");
|
|
||||||
processamentoFinalizado = false;
|
|
||||||
document.getElementById("btn-selecionar").disabled = false;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function baixarPlanilha() {
|
function baixarPlanilha() {
|
||||||
@@ -223,63 +102,21 @@ async function processar(btn) {
|
|||||||
window.open('/generate-report', '_blank');
|
window.open('/generate-report', '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
const dropZone = document.getElementById('upload-box');
|
||||||
updateStatus();
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
|
||||||
const dragOverlay = document.getElementById("drag-overlay");
|
|
||||||
let dragCounter = 0;
|
|
||||||
|
|
||||||
window.addEventListener("dragenter", e => {
|
|
||||||
dragCounter++;
|
|
||||||
dragOverlay.classList.add("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("dragleave", e => {
|
|
||||||
dragCounter--;
|
|
||||||
if (dragCounter <= 0) {
|
|
||||||
dragOverlay.classList.remove("active");
|
|
||||||
dragCounter = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("dragover", e => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('dragover');
|
||||||
});
|
});
|
||||||
|
dropZone.addEventListener('dragleave', () => {
|
||||||
window.addEventListener("drop", e => {
|
dropZone.classList.remove('dragover');
|
||||||
|
});
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dragOverlay.classList.remove("active");
|
dropZone.classList.remove('dragover');
|
||||||
dragCounter = 0;
|
|
||||||
handleFiles(e.dataTransfer.files);
|
handleFiles(e.dataTransfer.files);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
async function limparFaturas() {
|
|
||||||
if (!confirm("Deseja realmente limpar todas as faturas e arquivos (somente homologação)?")) return;
|
|
||||||
|
|
||||||
const res = await fetch("/limpar-faturas", { method: "POST" });
|
|
||||||
const data = await res.json();
|
|
||||||
alert(data.message || "Concluído");
|
|
||||||
updateStatus(); // atualiza visual
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("dragover", e => {
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("drop", e => {
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
function fecharFeedback() {
|
|
||||||
document.getElementById("upload-feedback").classList.add("hidden");
|
|
||||||
document.getElementById("tabela-faturas")?.classList.remove("tabela-bloqueada");
|
|
||||||
document.getElementById("tabela-wrapper").classList.remove("bloqueada");
|
|
||||||
document.getElementById("overlay-bloqueio").classList.add("hidden");
|
|
||||||
document.getElementById("barra-progresso").style.width = "0%";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', updateStatus);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -342,201 +179,6 @@ function fecharFeedback() {
|
|||||||
.status-ok { color: #198754; }
|
.status-ok { color: #198754; }
|
||||||
.status-error { color: #dc3545; }
|
.status-error { color: #dc3545; }
|
||||||
.status-warn { color: #ffc107; }
|
.status-warn { color: #ffc107; }
|
||||||
|
|
||||||
.status-processing {
|
|
||||||
color: #0d6efd; /* azul */
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
|
||||||
background-color: rgba(67, 97, 238, 0.7); /* institucional */
|
|
||||||
z-index: 9999;
|
|
||||||
display: none;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-overlay.active {
|
|
||||||
display: flex;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-overlay-content {
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
animation: fadeInUp 0.3s ease;
|
|
||||||
text-shadow: 1px 1px 6px rgba(0, 0, 0, 0.4);
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-overlay-content svg {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
fill: white;
|
|
||||||
filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.4));
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
transform: translateY(10px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback-popup {
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 99999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback-content {
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback-content h3 {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feedback-content p {
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabela-bloqueada {
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0.4;
|
|
||||||
filter: grayscale(0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabela-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-bloqueio {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
padding: 2rem 2rem 3rem 2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #555;
|
|
||||||
text-align: center;
|
|
||||||
z-index: 999;
|
|
||||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.15);
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#barra-progresso {
|
|
||||||
margin-top: 1.2rem;
|
|
||||||
height: 6px;
|
|
||||||
width: 0%;
|
|
||||||
background: linear-gradient(270deg, #4361ee, #66bbff, #4361ee);
|
|
||||||
background-size: 600% 600%;
|
|
||||||
animation: animarBarra 1.5s linear infinite;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: width 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grupo-status {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grupo-status summary {
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grupo-tabela {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grupo-tabela td {
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.barra-processamento {
|
|
||||||
height: 5px;
|
|
||||||
width: 0%; /* importante iniciar assim */
|
|
||||||
background: linear-gradient(270deg, #4361ee, #66bbff, #4361ee);
|
|
||||||
background-size: 600% 600%;
|
|
||||||
animation: animarBarra 1.5s linear infinite;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes animarBarra {
|
|
||||||
0% { background-position: 0% 50%; }
|
|
||||||
100% { background-position: 100% 50%; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse {
|
|
||||||
animation: pulseAnim 1.8s infinite;
|
|
||||||
}
|
|
||||||
@keyframes pulseAnim {
|
|
||||||
0% { transform: scale(1); }
|
|
||||||
50% { transform: scale(1.06); }
|
|
||||||
100% { transform: scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="drag-overlay" id="drag-overlay">
|
|
||||||
<div class="drag-overlay-content">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="#fff" viewBox="0 0 24 24">
|
|
||||||
<path d="M12 2C6.477 2 2 6.477 2 12c0 5.522 4.477 10 10 10s10-4.478 10-10c0-5.523-4.477-10-10-10Zm0 13a1 1 0 0 1-1-1v-2.586l-.293.293a1 1 0 0 1-1.414-1.414l2.707-2.707a1 1 0 0 1 1.414 0l2.707 2.707a1 1 0 0 1-1.414 1.414L13 11.414V14a1 1 0 0 1-1 1Z"/>
|
|
||||||
</svg>
|
|
||||||
<p>Solte os arquivos para enviar</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="upload-feedback" class="feedback-popup hidden">
|
|
||||||
<div class="feedback-content">
|
|
||||||
<h3>📦 Upload concluído!</h3>
|
|
||||||
<p id="feedback-sucesso">✔️ 0 enviados com sucesso</p>
|
|
||||||
<p id="feedback-erro">❌ 0 com erro(s)</p>
|
|
||||||
<p id="feedback-duplicado">📄 0 duplicado(s)</p>
|
|
||||||
<button onclick="fecharFeedback()" class="btn btn-primary" style="margin-top: 1rem;">OK</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -3,10 +3,10 @@ import fitz
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.database import AsyncSessionLocal
|
from database import AsyncSessionLocal
|
||||||
from app.models import Fatura, LogProcessamento
|
from models import Fatura, LogProcessamento
|
||||||
from app.calculos import calcular_campos_dinamicos
|
from calculos import calcular_campos_dinamicos
|
||||||
from app.layouts.equatorial_go import extrair_dados as extrair_dados_equatorial
|
from layouts.equatorial_go import extrair_dados as extrair_dados_equatorial
|
||||||
from sqlalchemy.future import select
|
from sqlalchemy.future import select
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
92
calculos.py
Executable file
92
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')
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Caminho base
|
|
||||||
$base = Get-Location
|
|
||||||
$appFolder = Join-Path $base "app"
|
|
||||||
|
|
||||||
# Arquivos candidatos na raiz
|
|
||||||
$arquivosRaiz = Get-ChildItem -Path $base -File -Filter *.py
|
|
||||||
|
|
||||||
foreach ($arquivo in $arquivosRaiz) {
|
|
||||||
$destino = Join-Path $appFolder $arquivo.Name
|
|
||||||
|
|
||||||
if (-Not (Test-Path $destino)) {
|
|
||||||
Write-Host "🟢 Movendo novo arquivo para app/: $($arquivo.Name)"
|
|
||||||
Move-Item $arquivo.FullName $destino
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$modificadoRaiz = (Get-Item $arquivo.FullName).LastWriteTime
|
|
||||||
$modificadoApp = (Get-Item $destino).LastWriteTime
|
|
||||||
|
|
||||||
if ($modificadoRaiz -gt $modificadoApp) {
|
|
||||||
Write-Host "🔄 Substituindo por versão mais recente: $($arquivo.Name)"
|
|
||||||
Move-Item -Force $arquivo.FullName $destino
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Host "⚪ Ignorando $($arquivo.Name) (versão dentro de app/ é mais nova)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`n✅ Finalizado. Revise a pasta app/ e apague os arquivos da raiz se desejar."
|
|
||||||
15
database.py
Executable file
15
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/app_faturas"
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,14 +1,4 @@
|
|||||||
# deploy-homolog.ps1
|
$message = Read-Host "Digite a descrição do commit para homologação"
|
||||||
$source = "$PSScriptRoot\app\*"
|
git add .
|
||||||
$destination = "\\216.22.5.141\home\app_fatura_homolog\app"
|
git commit -m "$message"
|
||||||
|
git push origin main
|
||||||
# (1) Envia somente a pasta `app/`
|
|
||||||
Copy-Item -Path $source -Destination $destination -Recurse -Force
|
|
||||||
|
|
||||||
# (2) Envia arquivos raiz essenciais (excluindo os antigos bagunçados)
|
|
||||||
$arquivos = @(".env", "requirements.txt", "docker-compose.yml", "Dockerfile", ".drone.yml")
|
|
||||||
foreach ($arquivo in $arquivos) {
|
|
||||||
Copy-Item "$PSScriptRoot\$arquivo" "\\216.22.5.141\home\app_fatura_homolog\" -Force
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Deploy concluído para homologação."
|
|
||||||
|
|||||||
BIN
deploy.log
BIN
deploy.log
Binary file not shown.
BIN
drone-debug.log
BIN
drone-debug.log
Binary file not shown.
132
layouts/equatorial_go.py
Executable file
132
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
|
||||||
|
|
||||||
|
|
||||||
136
main.py
Normal file
136
main.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
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 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)
|
||||||
|
def parametros_page(request: Request):
|
||||||
|
return templates.TemplateResponse("parametros.html", {"request": request})
|
||||||
|
|
||||||
|
@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"}
|
||||||
|
)
|
||||||
BIN
modelos_fatura/equatorial_goias_2025.pdf
Normal file
BIN
modelos_fatura/equatorial_goias_2025.pdf
Normal file
Binary file not shown.
BIN
modelos_fatura/equatorial_para_2025.pdf
Normal file
BIN
modelos_fatura/equatorial_para_2025.pdf
Normal file
Binary file not shown.
80
models.py
Executable file
80
models.py
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
# 📄 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)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
BIN
planilha_faturas.xlsx
Normal file
BIN
planilha_faturas.xlsx
Normal file
Binary file not shown.
93
processor.py
Normal file
93
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("app", "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()
|
||||||
79
relatorio_processamento.txt
Normal file
79
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
|
||||||
|
--------------------------------------------------
|
||||||
90
routes/dashboard.py
Executable file
90
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
routes/export.py
Executable file
32
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
routes/parametros.py
Executable file
83
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
routes/relatorios.py
Executable file
84
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
routes/selic.py
Executable file
66
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
startup.py
Executable file
10
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
static/cloud-upload.svg
Normal file
6
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
templates/dashboard.html
Executable file
107
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
templates/index.html
Normal file
184
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
templates/index2.html
Executable file
201
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>
|
||||||
26
templates/parametros.html
Executable file
26
templates/parametros.html
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends "index.html" %}
|
||||||
|
{% block title %}Parâmetros de Cálculo{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>⚙️ Parâmetros</h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<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 '' }}" required/><br><br>
|
||||||
|
|
||||||
|
<label for="formula_pis">Fórmula PIS:</label><br>
|
||||||
|
<input type="text" name="formula_pis" id="formula_pis" value="{{ parametros.formula_pis or '' }}" required/><br><br>
|
||||||
|
|
||||||
|
<label for="formula_cofins">Fórmula COFINS:</label><br>
|
||||||
|
<input type="text" name="formula_cofins" id="formula_cofins" value="{{ parametros.formula_cofins or '' }}" required/><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
templates/relatorios.html
Executable file
38
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
templates/upload.html
Executable file
184
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
testar_fatura.py
Executable file
20
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
transferring
Normal file
0
transferring
Normal file
1
trigger.txt
Normal file
1
trigger.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Trigger test Mon Jul 28 07:10:34 PM -03 2025
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
uploads/temp/2020016069139.pdf
Executable file
BIN
uploads/temp/2020016069139.pdf
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
65
utils.py
Executable file
65
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
|
||||||
BIN
~$planilha_faturas.xlsx
Executable file
BIN
~$planilha_faturas.xlsx
Executable file
Binary file not shown.
Reference in New Issue
Block a user