Compare commits
6 Commits
cddb37ab91
...
8e218be1ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e218be1ce | ||
|
|
a3269b45c3 | ||
|
|
e558fcde56 | ||
|
|
60a11f89ff | ||
|
|
59ceb7a51f | ||
|
|
b61c6d4abc |
26
.drone.yml
26
.drone.yml
@@ -1,11 +1,11 @@
|
|||||||
---
|
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: homolog
|
name: deploy
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
- main
|
- main
|
||||||
|
- production
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: deploy to homolog
|
- name: deploy to homolog
|
||||||
@@ -18,6 +18,8 @@ steps:
|
|||||||
source: .
|
source: .
|
||||||
target: /home/app_fatura_homolog
|
target: /home/app_fatura_homolog
|
||||||
rm: true
|
rm: true
|
||||||
|
when:
|
||||||
|
branch: main
|
||||||
|
|
||||||
- name: restart homolog container
|
- name: restart homolog container
|
||||||
image: appleboy/drone-ssh
|
image: appleboy/drone-ssh
|
||||||
@@ -27,20 +29,12 @@ steps:
|
|||||||
password: F6tC5tCh29XQRpzp
|
password: F6tC5tCh29XQRpzp
|
||||||
port: 22
|
port: 22
|
||||||
script:
|
script:
|
||||||
- docker rm -f FaturasHomolog || true
|
|
||||||
- cd /home/app_fatura_homolog
|
- cd /home/app_fatura_homolog
|
||||||
|
- docker compose down --remove-orphans
|
||||||
- docker compose up -d
|
- docker compose up -d
|
||||||
|
when:
|
||||||
|
branch: main
|
||||||
|
|
||||||
---
|
|
||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: production
|
|
||||||
|
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- production
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: deploy to production
|
- name: deploy to production
|
||||||
image: appleboy/drone-scp
|
image: appleboy/drone-scp
|
||||||
settings:
|
settings:
|
||||||
@@ -51,6 +45,8 @@ steps:
|
|||||||
source: .
|
source: .
|
||||||
target: /home/app_fatura
|
target: /home/app_fatura
|
||||||
rm: true
|
rm: true
|
||||||
|
when:
|
||||||
|
branch: production
|
||||||
|
|
||||||
- name: restart production container
|
- name: restart production container
|
||||||
image: appleboy/drone-ssh
|
image: appleboy/drone-ssh
|
||||||
@@ -60,7 +56,9 @@ steps:
|
|||||||
password: F6tC5tCh29XQRpzp
|
password: F6tC5tCh29XQRpzp
|
||||||
port: 22
|
port: 22
|
||||||
script:
|
script:
|
||||||
- docker rm -f Faturas || true
|
|
||||||
- cd /home/app_fatura
|
- cd /home/app_fatura
|
||||||
|
- docker compose down --remove-orphans
|
||||||
- docker compose up -d
|
- docker compose up -d
|
||||||
|
when:
|
||||||
|
branch: production
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
# trigger novo build Drone CI
|
# trigger novo build Drone CI
|
||||||
# teste de produção
|
# teste de homologação
|
||||||
# novo teste
|
|
||||||
# novo teste
|
|
||||||
|
|||||||
BIN
app/.main.py.swn
BIN
app/.main.py.swn
Binary file not shown.
BIN
app/.main.py.swo
BIN
app/.main.py.swo
Binary file not shown.
BIN
app/.main.py.swp
BIN
app/.main.py.swp
Binary file not shown.
@@ -1,22 +0,0 @@
|
|||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
python3-dev \
|
|
||||||
libffi-dev \
|
|
||||||
libmupdf-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
RUN pip install --upgrade pip
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "5000"]
|
|
||||||
5834
app/app.log
5834
app/app.log
File diff suppressed because it is too large
Load Diff
@@ -1,92 +0,0 @@
|
|||||||
# 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,15 +0,0 @@
|
|||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
||||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
||||||
from contextlib import contextmanager
|
|
||||||
|
|
||||||
# database.py
|
|
||||||
DATABASE_URL = "postgresql+asyncpg://fatura:102030@ic-postgresql-FtOY:5432/producao"
|
|
||||||
|
|
||||||
async_engine = create_async_engine(DATABASE_URL, echo=False, future=True)
|
|
||||||
AsyncSessionLocal = sessionmaker(bind=async_engine, class_=AsyncSession, expire_on_commit=False)
|
|
||||||
Base = declarative_base()
|
|
||||||
|
|
||||||
async def get_session():
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
yield session
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
version: '3.9'
|
|
||||||
|
|
||||||
services:
|
|
||||||
app_fatura:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: Faturas
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
volumes:
|
|
||||||
- ./uploads:/app/uploads
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- icontainer-network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
icontainer-network:
|
|
||||||
external: true
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# app/layouts/equatorial_go.py
|
|
||||||
import re
|
|
||||||
from datetime import datetime
|
|
||||||
import fitz
|
|
||||||
import logging
|
|
||||||
|
|
||||||
def converter_valor(valor_str):
|
|
||||||
try:
|
|
||||||
if not valor_str:
|
|
||||||
return 0.0
|
|
||||||
valor_limpo = str(valor_str).replace('.', '').replace(',', '.')
|
|
||||||
valor_limpo = re.sub(r'[^\d.]', '', valor_limpo)
|
|
||||||
return float(valor_limpo) if valor_limpo else 0.0
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
def extrair_dados(texto_final):
|
|
||||||
import logging
|
|
||||||
logging.debug("\n========== INÍCIO DO TEXTO EXTRAÍDO ==========\n" + texto_final + "\n========== FIM ==========")
|
|
||||||
|
|
||||||
def extrair_seguro(patterns, texto_busca, flags=re.IGNORECASE | re.MULTILINE):
|
|
||||||
if not isinstance(patterns, list): patterns = [patterns]
|
|
||||||
for pattern in patterns:
|
|
||||||
match = re.search(pattern, texto_busca, flags)
|
|
||||||
if match:
|
|
||||||
for group in match.groups():
|
|
||||||
if group: return group.strip()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
nota_fiscal = extrair_seguro(r'NOTA FISCAL Nº\s*(\d+)', texto_final)
|
|
||||||
|
|
||||||
uc = extrair_seguro([
|
|
||||||
r'(\d{7,10}-\d)',
|
|
||||||
r'UNIDADE\s+CONSUMIDORA\s*[:\-]?\s*(\d{6,})',
|
|
||||||
r'(\d{6,})\s+FAZENDA',
|
|
||||||
r'(\d{6,})\s+AVENIDA',
|
|
||||||
r'(\d{6,})\s+RUA'
|
|
||||||
], texto_final)
|
|
||||||
|
|
||||||
logging.debug("TEXTO PDF:\n" + texto_final)
|
|
||||||
|
|
||||||
referencia = extrair_seguro([
|
|
||||||
r'\b([A-Z]{3}/\d{4})\b',
|
|
||||||
r'\b([A-Z]{3}\s*/\s*\d{4})\b',
|
|
||||||
r'\b([A-Z]{3}\d{4})\b',
|
|
||||||
r'(\d{2}/\d{4})'
|
|
||||||
], texto_final.upper())
|
|
||||||
|
|
||||||
# Limpeza prévia para evitar falhas por quebra de linha ou espaços
|
|
||||||
texto_limpo = texto_final.replace('\n', '').replace('\r', '').replace(' ', '')
|
|
||||||
|
|
||||||
if any(padrao in texto_limpo for padrao in ['R$***********0,00', 'R$*********0,00', 'R$*0,00']):
|
|
||||||
valor_total = 0.0
|
|
||||||
else:
|
|
||||||
match_valor_total = re.search(r'R\$[*\s]*([\d\.\s]*,\d{2})', texto_final)
|
|
||||||
valor_total = converter_valor(match_valor_total.group(1)) if match_valor_total else None
|
|
||||||
|
|
||||||
match_nome = re.search(r'(?<=\n)([A-Z\s]{10,})(?=\s+CNPJ/CPF:)', texto_final)
|
|
||||||
nome = match_nome.group(1).replace('\n', ' ').strip() if match_nome else "NÃO IDENTIFICADO"
|
|
||||||
|
|
||||||
# Remove qualquer excesso após o nome verdadeiro
|
|
||||||
nome = re.split(r'\b(FAZENDA|RUA|AVENIDA|SETOR|CEP|CNPJ|CPF)\b', nome, maxsplit=1, flags=re.IGNORECASE)[0].strip()
|
|
||||||
|
|
||||||
match_cidade_estado = re.search(r'CEP:\s*\d{8}\s+(.*?)\s+([A-Z]{2})\s+BRASIL', texto_final)
|
|
||||||
cidade = match_cidade_estado.group(1).strip() if match_cidade_estado else "NÃO IDENTIFICADA"
|
|
||||||
estado = match_cidade_estado.group(2).strip() if match_cidade_estado else "NÃO IDENTIFICADO"
|
|
||||||
|
|
||||||
match_class = re.search(r'Classificação:\s*(.*)', texto_final, re.IGNORECASE)
|
|
||||||
classificacao = match_class.group(1).strip() if match_class else "NÃO IDENTIFICADA"
|
|
||||||
|
|
||||||
def extrair_tributo_linhas_separadas(nome_tributo):
|
|
||||||
linhas = texto_final.split('\n')
|
|
||||||
for i, linha in enumerate(linhas):
|
|
||||||
if nome_tributo in linha.upper():
|
|
||||||
aliq = base = valor = 0.0
|
|
||||||
if i + 1 < len(linhas):
|
|
||||||
aliq_match = re.search(r'([\d,]+)%', linhas[i + 1])
|
|
||||||
if aliq_match:
|
|
||||||
aliq = converter_valor(aliq_match.group(1)) / 100
|
|
||||||
if i + 2 < len(linhas):
|
|
||||||
base = converter_valor(linhas[i + 2].strip())
|
|
||||||
if i + 3 < len(linhas):
|
|
||||||
valor = converter_valor(linhas[i + 3].strip())
|
|
||||||
return base, aliq, valor
|
|
||||||
return 0.0, 0.0, 0.0
|
|
||||||
|
|
||||||
pis_base, pis_aliq, pis_valor = extrair_tributo_linhas_separadas('PIS/PASEP')
|
|
||||||
icms_base, icms_aliq, icms_valor = extrair_tributo_linhas_separadas('ICMS')
|
|
||||||
cofins_base, cofins_aliq, cofins_valor = extrair_tributo_linhas_separadas('COFINS')
|
|
||||||
|
|
||||||
match_consumo = re.search(r'CONSUMO\s+\d+\s+([\d.,]+)', texto_final)
|
|
||||||
consumo = converter_valor(match_consumo.group(1)) if match_consumo else 0.0
|
|
||||||
|
|
||||||
match_tarifa = re.search(r'CONSUMO KWH \+ ICMS/PIS/COFINS\s+([\d.,]+)', texto_final) \
|
|
||||||
or re.search(r'CUSTO DISP\s+([\d.,]+)', texto_final)
|
|
||||||
tarifa = converter_valor(match_tarifa.group(1)) if match_tarifa else 0.0
|
|
||||||
|
|
||||||
dados = {
|
|
||||||
'classificacao_tarifaria': classificacao,
|
|
||||||
'nome': nome,
|
|
||||||
'unidade_consumidora': uc,
|
|
||||||
'cidade': cidade,
|
|
||||||
'estado': estado,
|
|
||||||
'referencia': referencia,
|
|
||||||
'valor_total': valor_total,
|
|
||||||
'pis_aliq': pis_aliq,
|
|
||||||
'icms_aliq': icms_aliq,
|
|
||||||
'cofins_aliq': cofins_aliq,
|
|
||||||
'pis_valor': pis_valor,
|
|
||||||
'icms_valor': icms_valor,
|
|
||||||
'cofins_valor': cofins_valor,
|
|
||||||
'pis_base': pis_base,
|
|
||||||
'icms_base': icms_base,
|
|
||||||
'cofins_base': cofins_base,
|
|
||||||
'consumo': consumo,
|
|
||||||
'tarifa': tarifa,
|
|
||||||
'nota_fiscal': nota_fiscal,
|
|
||||||
'data_processamento': datetime.now(),
|
|
||||||
'distribuidora': 'Equatorial Goiás'
|
|
||||||
}
|
|
||||||
|
|
||||||
campos_obrigatorios = ['nome', 'unidade_consumidora', 'referencia', 'nota_fiscal']
|
|
||||||
faltantes = [campo for campo in campos_obrigatorios if not dados.get(campo)]
|
|
||||||
if valor_total is None and not any(p in texto_limpo for p in ['R$***********0,00', 'R$*********0,00', 'R$*0,00']):
|
|
||||||
faltantes.append('valor_total')
|
|
||||||
|
|
||||||
if faltantes:
|
|
||||||
raise ValueError(f"Campos obrigatórios faltantes: {', '.join(faltantes)}")
|
|
||||||
|
|
||||||
return dados
|
|
||||||
|
|
||||||
|
|
||||||
197
app/main.py
197
app/main.py
@@ -1,197 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from fastapi import FastAPI, Request, UploadFile, File
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
import os, shutil
|
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
from io import BytesIO
|
|
||||||
import pandas as pd
|
|
||||||
from sqlalchemy.future import select
|
|
||||||
from database import AsyncSessionLocal
|
|
||||||
from models import Fatura
|
|
||||||
from models import ParametrosFormula
|
|
||||||
from fastapi import Form
|
|
||||||
from types import SimpleNamespace
|
|
||||||
from processor import (
|
|
||||||
fila_processamento,
|
|
||||||
processar_em_lote,
|
|
||||||
status_arquivos,
|
|
||||||
limpar_arquivos_processados
|
|
||||||
)
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
||||||
|
|
||||||
UPLOAD_DIR = "uploads/temp"
|
|
||||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
|
||||||
def dashboard(request: Request):
|
|
||||||
indicadores = [
|
|
||||||
{"titulo": "Total de Faturas", "valor": 124},
|
|
||||||
{"titulo": "Faturas com ICMS", "valor": "63%"},
|
|
||||||
{"titulo": "Valor Total", "valor": "R$ 280.000,00"},
|
|
||||||
]
|
|
||||||
|
|
||||||
analise_stf = {
|
|
||||||
"antes": {"percentual_com_icms": 80, "media_valor": 1200},
|
|
||||||
"depois": {"percentual_com_icms": 20, "media_valor": 800},
|
|
||||||
}
|
|
||||||
|
|
||||||
return templates.TemplateResponse("dashboard.html", {
|
|
||||||
"request": request,
|
|
||||||
"cliente_atual": "",
|
|
||||||
"clientes": ["Cliente A", "Cliente B"],
|
|
||||||
"indicadores": indicadores,
|
|
||||||
"analise_stf": analise_stf
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.get("/upload", response_class=HTMLResponse)
|
|
||||||
def upload_page(request: Request):
|
|
||||||
return templates.TemplateResponse("upload.html", {"request": request})
|
|
||||||
|
|
||||||
@app.get("/relatorios", response_class=HTMLResponse)
|
|
||||||
def relatorios_page(request: Request):
|
|
||||||
return templates.TemplateResponse("relatorios.html", {"request": request})
|
|
||||||
|
|
||||||
@app.get("/parametros", response_class=HTMLResponse)
|
|
||||||
async def parametros_page(request: Request):
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
result = await session.execute(select(ParametrosFormula).limit(1))
|
|
||||||
parametros = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
return templates.TemplateResponse("parametros.html", {
|
|
||||||
"request": request,
|
|
||||||
"parametros": parametros or SimpleNamespace(
|
|
||||||
aliquota_icms=None,
|
|
||||||
incluir_icms=True,
|
|
||||||
incluir_pis=True,
|
|
||||||
incluir_cofins=True
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.post("/upload-files")
|
|
||||||
async def upload_files(files: list[UploadFile] = File(...)):
|
|
||||||
for file in files:
|
|
||||||
temp_path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4()}_{file.filename}")
|
|
||||||
with open(temp_path, "wb") as f:
|
|
||||||
shutil.copyfileobj(file.file, f)
|
|
||||||
await fila_processamento.put({
|
|
||||||
"caminho_pdf": temp_path,
|
|
||||||
"nome_original": file.filename
|
|
||||||
})
|
|
||||||
return {"message": "Arquivos enviados para fila"}
|
|
||||||
|
|
||||||
@app.post("/process-queue")
|
|
||||||
async def process_queue():
|
|
||||||
resultados = await processar_em_lote()
|
|
||||||
return {"message": "Processamento concluído", "resultados": resultados}
|
|
||||||
|
|
||||||
@app.get("/get-status")
|
|
||||||
async def get_status():
|
|
||||||
files = []
|
|
||||||
for nome, status in status_arquivos.items():
|
|
||||||
files.append({
|
|
||||||
"nome": nome,
|
|
||||||
"status": status,
|
|
||||||
"mensagem": "---" if status == "Concluído" else status
|
|
||||||
})
|
|
||||||
is_processing = not fila_processamento.empty()
|
|
||||||
return JSONResponse(content={"is_processing": is_processing, "files": files})
|
|
||||||
|
|
||||||
@app.post("/clear-all")
|
|
||||||
async def clear_all():
|
|
||||||
limpar_arquivos_processados()
|
|
||||||
for f in os.listdir(UPLOAD_DIR):
|
|
||||||
os.remove(os.path.join(UPLOAD_DIR, f))
|
|
||||||
return {"message": "Fila e arquivos limpos"}
|
|
||||||
|
|
||||||
@app.get("/export-excel")
|
|
||||||
async def export_excel():
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
result = await session.execute(select(Fatura))
|
|
||||||
faturas = result.scalars().all()
|
|
||||||
|
|
||||||
dados = []
|
|
||||||
for f in faturas:
|
|
||||||
dados.append({
|
|
||||||
"Nome": f.nome,
|
|
||||||
"UC": f.unidade_consumidora,
|
|
||||||
"Referência": f.referencia,
|
|
||||||
"Nota Fiscal": f.nota_fiscal,
|
|
||||||
"Valor Total": f.valor_total,
|
|
||||||
"ICMS (%)": f.icms_aliq,
|
|
||||||
"ICMS (R$)": f.icms_valor,
|
|
||||||
"Base ICMS": f.icms_base,
|
|
||||||
"PIS (%)": f.pis_aliq,
|
|
||||||
"PIS (R$)": f.pis_valor,
|
|
||||||
"Base PIS": f.pis_base,
|
|
||||||
"COFINS (%)": f.cofins_aliq,
|
|
||||||
"COFINS (R$)": f.cofins_valor,
|
|
||||||
"Base COFINS": f.cofins_base,
|
|
||||||
"Consumo (kWh)": f.consumo,
|
|
||||||
"Tarifa": f.tarifa,
|
|
||||||
"Cidade": f.cidade,
|
|
||||||
"Estado": f.estado,
|
|
||||||
"Distribuidora": f.distribuidora,
|
|
||||||
"Data Processamento": f.data_processamento,
|
|
||||||
})
|
|
||||||
|
|
||||||
df = pd.DataFrame(dados)
|
|
||||||
output = BytesIO()
|
|
||||||
df.to_excel(output, index=False, sheet_name="Faturas")
|
|
||||||
output.seek(0)
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
output,
|
|
||||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
||||||
headers={"Content-Disposition": "attachment; filename=relatorio_faturas.xlsx"}
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.post("/parametros", response_class=HTMLResponse)
|
|
||||||
async def salvar_parametros(
|
|
||||||
request: Request,
|
|
||||||
aliquota_icms: float = Form(...),
|
|
||||||
formula_pis: str = Form(...),
|
|
||||||
formula_cofins: str = Form(...)
|
|
||||||
):
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
result = await session.execute(select(ParametrosFormula).limit(1))
|
|
||||||
existente = result.scalar_one_or_none()
|
|
||||||
|
|
||||||
if existente:
|
|
||||||
existente.aliquota_icms = aliquota_icms
|
|
||||||
existente.incluir_icms = 1
|
|
||||||
existente.incluir_pis = 1
|
|
||||||
existente.incluir_cofins = 1
|
|
||||||
existente.formula = f"PIS={formula_pis};COFINS={formula_cofins}"
|
|
||||||
mensagem = "Parâmetros atualizados com sucesso."
|
|
||||||
else:
|
|
||||||
novo = ParametrosFormula(
|
|
||||||
aliquota_icms=aliquota_icms,
|
|
||||||
incluir_icms=1,
|
|
||||||
incluir_pis=1,
|
|
||||||
incluir_cofins=1,
|
|
||||||
formula=f"PIS={formula_pis};COFINS={formula_cofins}"
|
|
||||||
)
|
|
||||||
session.add(novo)
|
|
||||||
mensagem = "Parâmetros salvos com sucesso."
|
|
||||||
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
parametros = SimpleNamespace(
|
|
||||||
aliquota_icms=aliquota_icms,
|
|
||||||
incluir_icms=1,
|
|
||||||
incluir_pis=1,
|
|
||||||
incluir_cofins=1,
|
|
||||||
formula_pis=formula_pis,
|
|
||||||
formula_cofins=formula_cofins
|
|
||||||
)
|
|
||||||
|
|
||||||
return templates.TemplateResponse("parametros.html", {
|
|
||||||
"request": request,
|
|
||||||
"parametros": parametros,
|
|
||||||
"mensagem": mensagem
|
|
||||||
})
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,85 +0,0 @@
|
|||||||
# 📄 models.py
|
|
||||||
from sqlalchemy import Column, String, Integer, Float, DateTime, Text
|
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
|
||||||
from database import Base
|
|
||||||
|
|
||||||
class ParametrosFormula(Base):
|
|
||||||
__tablename__ = 'parametros_formula'
|
|
||||||
__table_args__ = {'schema': 'faturas', 'extend_existing': True}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
nome = Column(String)
|
|
||||||
formula = Column(Text)
|
|
||||||
|
|
||||||
# Novos campos
|
|
||||||
aliquota_icms = Column(Float)
|
|
||||||
incluir_icms = Column(Integer) # Use Boolean se preferir
|
|
||||||
incluir_pis = Column(Integer)
|
|
||||||
incluir_cofins = Column(Integer)
|
|
||||||
|
|
||||||
class Fatura(Base):
|
|
||||||
__tablename__ = "faturas"
|
|
||||||
__table_args__ = {'schema': 'faturas'}
|
|
||||||
|
|
||||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
||||||
nome = Column(String)
|
|
||||||
classificacao_tarifaria = Column("classificacao_tarifaria", String)
|
|
||||||
unidade_consumidora = Column("unidade_consumidora", String)
|
|
||||||
referencia = Column(String)
|
|
||||||
valor_total = Column(Float)
|
|
||||||
|
|
||||||
pis_aliq = Column("pis_aliq", Float)
|
|
||||||
pis_valor = Column("pis_valor", Float)
|
|
||||||
pis_base = Column("pis_base", Float)
|
|
||||||
|
|
||||||
icms_aliq = Column("icms_aliq", Float)
|
|
||||||
icms_valor = Column("icms_valor", Float)
|
|
||||||
icms_base = Column("icms_base", Float)
|
|
||||||
|
|
||||||
cofins_aliq = Column("cofins_aliq", Float)
|
|
||||||
cofins_valor = Column("cofins_valor", Float)
|
|
||||||
cofins_base = Column("cofins_base", Float)
|
|
||||||
|
|
||||||
consumo = Column("consumo", Float)
|
|
||||||
tarifa = Column("tarifa", Float)
|
|
||||||
|
|
||||||
nota_fiscal = Column(String)
|
|
||||||
data_processamento = Column(DateTime, default=datetime.utcnow)
|
|
||||||
arquivo_pdf = Column("arquivo_pdf", String)
|
|
||||||
cidade = Column(String)
|
|
||||||
estado = Column(String)
|
|
||||||
distribuidora = Column(String)
|
|
||||||
link_arquivo = Column("link_arquivo", String)
|
|
||||||
|
|
||||||
|
|
||||||
class LogProcessamento(Base):
|
|
||||||
__tablename__ = "logs_processamento"
|
|
||||||
__table_args__ = {'schema': 'faturas'}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
nome_arquivo = Column(String)
|
|
||||||
status = Column(String)
|
|
||||||
mensagem = Column(Text)
|
|
||||||
acao = Column(String) # nova coluna existente no banco
|
|
||||||
data_log = Column("data_log", DateTime, default=datetime.utcnow)
|
|
||||||
|
|
||||||
|
|
||||||
class AliquotaUF(Base):
|
|
||||||
__tablename__ = "aliquotas_uf"
|
|
||||||
__table_args__ = {'schema': 'faturas'}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
uf = Column(String)
|
|
||||||
exercicio = Column(String)
|
|
||||||
aliquota = Column(Float)
|
|
||||||
|
|
||||||
class SelicMensal(Base):
|
|
||||||
__tablename__ = "selic_mensal"
|
|
||||||
__table_args__ = {'schema': 'faturas'}
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
ano = Column(Integer)
|
|
||||||
mes = Column(Integer)
|
|
||||||
fator = Column(Float)
|
|
||||||
Binary file not shown.
@@ -1,93 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import asyncio
|
|
||||||
from sqlalchemy.future import select
|
|
||||||
from utils import extrair_dados_pdf
|
|
||||||
from database import AsyncSessionLocal
|
|
||||||
from models import Fatura, LogProcessamento
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
UPLOADS_DIR = os.path.join(os.getcwd(), "uploads")
|
|
||||||
TEMP_DIR = os.path.join(UPLOADS_DIR, "temp")
|
|
||||||
|
|
||||||
fila_processamento = asyncio.Queue()
|
|
||||||
status_arquivos = {}
|
|
||||||
|
|
||||||
def remover_arquivo_temp(caminho_pdf):
|
|
||||||
try:
|
|
||||||
if os.path.exists(caminho_pdf) and TEMP_DIR in caminho_pdf:
|
|
||||||
os.remove(caminho_pdf)
|
|
||||||
logger.info(f"Arquivo temporário removido: {os.path.basename(caminho_pdf)}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Falha ao remover arquivo temporário: {e}")
|
|
||||||
|
|
||||||
def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal):
|
|
||||||
try:
|
|
||||||
extensao = os.path.splitext(nome_original)[1].lower()
|
|
||||||
nome_destino = f"{nota_fiscal}{extensao}"
|
|
||||||
destino_final = os.path.join(UPLOADS_DIR, nome_destino)
|
|
||||||
shutil.copy2(caminho_pdf_temp, destino_final)
|
|
||||||
return destino_final
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Erro ao salvar em uploads: {e}")
|
|
||||||
return caminho_pdf_temp
|
|
||||||
|
|
||||||
async def process_single_file(caminho_pdf_temp: str, nome_original: str):
|
|
||||||
async with AsyncSessionLocal() as session:
|
|
||||||
try:
|
|
||||||
dados = extrair_dados_pdf(caminho_pdf_temp)
|
|
||||||
dados['arquivo_pdf'] = nome_original
|
|
||||||
|
|
||||||
existente_result = await session.execute(
|
|
||||||
select(Fatura).filter_by(nota_fiscal=dados['nota_fiscal'], unidade_consumidora=dados['unidade_consumidora'])
|
|
||||||
)
|
|
||||||
if existente_result.scalar_one_or_none():
|
|
||||||
remover_arquivo_temp(caminho_pdf_temp)
|
|
||||||
return {"status": "Duplicado", "dados": dados}
|
|
||||||
|
|
||||||
caminho_final = salvar_em_uploads(caminho_pdf_temp, nome_original, dados['nota_fiscal'])
|
|
||||||
dados['link_arquivo'] = caminho_final
|
|
||||||
|
|
||||||
|
|
||||||
fatura = Fatura(**dados)
|
|
||||||
session.add(fatura)
|
|
||||||
|
|
||||||
session.add(LogProcessamento(
|
|
||||||
status="Sucesso",
|
|
||||||
mensagem="Fatura processada com sucesso",
|
|
||||||
nome_arquivo=nome_original,
|
|
||||||
acao="PROCESSAMENTO"
|
|
||||||
))
|
|
||||||
|
|
||||||
await session.commit()
|
|
||||||
remover_arquivo_temp(caminho_pdf_temp)
|
|
||||||
return {"status": "Concluído", "dados": dados}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
await session.rollback()
|
|
||||||
session.add(LogProcessamento(
|
|
||||||
status="Erro",
|
|
||||||
mensagem=str(e),
|
|
||||||
nome_arquivo=nome_original,
|
|
||||||
acao="PROCESSAMENTO"
|
|
||||||
))
|
|
||||||
await session.commit()
|
|
||||||
logger.error(f"Erro ao processar fatura: {e}", exc_info=True)
|
|
||||||
remover_arquivo_temp(caminho_pdf_temp)
|
|
||||||
return {"status": "Erro", "mensagem": str(e)}
|
|
||||||
|
|
||||||
async def processar_em_lote():
|
|
||||||
resultados = []
|
|
||||||
while not fila_processamento.empty():
|
|
||||||
item = await fila_processamento.get()
|
|
||||||
resultado = await process_single_file(item['caminho_pdf'], item['nome_original'])
|
|
||||||
status_arquivos[item['nome_original']] = resultado.get("status", "Erro")
|
|
||||||
resultados.append(resultado)
|
|
||||||
return resultados
|
|
||||||
|
|
||||||
def limpar_arquivos_processados():
|
|
||||||
status_arquivos.clear()
|
|
||||||
while not fila_processamento.empty():
|
|
||||||
fila_processamento.get_nowait()
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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
|
|
||||||
--------------------------------------------------
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
fastapi==0.110.0
|
|
||||||
uvicorn[standard]==0.29.0
|
|
||||||
jinja2==3.1.3
|
|
||||||
sqlalchemy==2.0.30
|
|
||||||
asyncpg==0.29.0
|
|
||||||
python-multipart==0.0.6
|
|
||||||
openpyxl==3.1.2
|
|
||||||
pandas==2.2.2
|
|
||||||
PyMuPDF==1.22.5
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
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
|
|
||||||
})
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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"})
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# 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"}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# 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"})
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# 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)}")
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 397 B |
@@ -1,107 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{% extends "index.html" %}
|
|
||||||
{% block title %}Parâmetros de Cálculo{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<h1>⚙️ Parâmetros</h1>
|
|
||||||
|
|
||||||
<form method="post">
|
|
||||||
<label for="tipo">Tipo:</label><br>
|
|
||||||
<input type="text" name="tipo" id="tipo" value="{{ parametros.tipo or '' }}" required/><br><br>
|
|
||||||
|
|
||||||
<label for="formula">Fórmula:</label><br>
|
|
||||||
<input type="text" name="formula" id="formula" value="{{ parametros.formula or '' }}" required/><br><br>
|
|
||||||
|
|
||||||
<label for="aliquota_icms">Alíquota de ICMS (%):</label><br>
|
|
||||||
<input type="number" step="0.01" name="aliquota_icms" id="aliquota_icms" value="{{ parametros.aliquota_icms or '' }}" /><br><br>
|
|
||||||
|
|
||||||
<label for="incluir_icms">Incluir ICMS:</label><br>
|
|
||||||
<input type="checkbox" name="incluir_icms" id="incluir_icms" value="1" {% if parametros.incluir_icms %}checked{% endif %}><br><br>
|
|
||||||
|
|
||||||
<label for="ativo">Ativo:</label><br>
|
|
||||||
<input type="checkbox" name="ativo" id="ativo" value="1" {% if parametros.ativo %}checked{% endif %}><br><br>
|
|
||||||
|
|
||||||
<button type="submit" style="padding: 10px 20px; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
|
||||||
Salvar Parâmetros
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% if mensagem %}
|
|
||||||
<div style="margin-top: 20px; background: #e0f7fa; padding: 10px; border-left: 4px solid #2563eb;">
|
|
||||||
{{ mensagem }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
{% 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 %}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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())
|
|
||||||
|
|
||||||
65
app/utils.py
65
app/utils.py
@@ -1,65 +0,0 @@
|
|||||||
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
|
|
||||||
Binary file not shown.
Reference in New Issue
Block a user