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