Spaces:
Running
Running
App Custo
Browse files- .gitattributes +1 -0
- .gitignore +0 -0
- BP - Analítico-App-Custo-v2 (1).png +3 -0
- Dockerfile +19 -0
- Google_Sheets.py +94 -0
- Login.py +153 -0
- Qlik_sense.py +147 -0
- Registros.py +251 -0
- login fundo.webp +0 -0
- main.py +559 -0
- requirements.txt +7 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
BP[[:space:]]-[[:space:]]Analítico-App-Custo-v2[[:space:]](1).png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
File without changes
|
BP - Analítico-App-Custo-v2 (1).png
ADDED
|
Git LFS Details
|
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 1. Pega um "computador" Linux com Python 3.11 já instalado
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# 2. Cria uma pasta chamada /app lá dentro e entra nela
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# 3. Copia a nossa lista de compras e manda o Python instalar tudo
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
+
|
| 11 |
+
# 4. Copia TODO o resto dos seus arquivos para dentro da nuvem
|
| 12 |
+
# (Isso vai copiar o main.py, suas imagens PNG, o Login.py, etc)
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
# 5. Avisa o Hugging Face que o nosso sistema vai sair pela porta 7860
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
|
| 18 |
+
# 6. O comando final: Dá o play no sistema!
|
| 19 |
+
CMD ["python", "main.py"]
|
Google_Sheets.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
import gspread
|
| 4 |
+
from google.oauth2.service_account import Credentials
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import json # <-- NOVO IMPORT OBRIGATÓRIO
|
| 7 |
+
|
| 8 |
+
# Abrindo o cofre .env para pegar as chaves (funciona no PC, é ignorado no Hugging Face)
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
# --- 1. CONFIGURAÇÕES ---
|
| 12 |
+
ID_PLANILHA = os.getenv("ID_PLANILHA")
|
| 13 |
+
|
| 14 |
+
# Escopos (permissões) que o robô vai pedir
|
| 15 |
+
ESCOPOS = [
|
| 16 |
+
"https://www.googleapis.com/auth/spreadsheets",
|
| 17 |
+
"https://www.googleapis.com/auth/drive"
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
def conectar_sheets():
|
| 21 |
+
"""Lê as credenciais (da nuvem ou local) e abre a conexão definitiva com o Google"""
|
| 22 |
+
try:
|
| 23 |
+
# Tenta puxar o JSON gigante direto do cofre (Hugging Face)
|
| 24 |
+
segredo_google = os.environ.get("GOOGLE_JSON")
|
| 25 |
+
|
| 26 |
+
if segredo_google:
|
| 27 |
+
# --- MODO NUVEM (Hugging Face) ---
|
| 28 |
+
print("Tentando conectar pelo MODO NUVEM...")
|
| 29 |
+
credenciais_dict = json.loads(segredo_google)
|
| 30 |
+
# Usa o from_service_account_info em vez do _file
|
| 31 |
+
credenciais = Credentials.from_service_account_info(credenciais_dict, scopes=ESCOPOS)
|
| 32 |
+
else:
|
| 33 |
+
# --- MODO LOCAL (Seu Computador) ---
|
| 34 |
+
print("Tentando conectar pelo MODO LOCAL...")
|
| 35 |
+
CAMINHO_JSON = os.getenv("CAMINHO_JSON")
|
| 36 |
+
credenciais = Credentials.from_service_account_file(CAMINHO_JSON, scopes=ESCOPOS)
|
| 37 |
+
|
| 38 |
+
# Autoriza e cria o cliente final
|
| 39 |
+
cliente = gspread.authorize(credenciais)
|
| 40 |
+
print("✅ Conectado ao Google com sucesso!")
|
| 41 |
+
return cliente
|
| 42 |
+
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"❌ Erro ao conectar: {e}")
|
| 45 |
+
return None
|
| 46 |
+
|
| 47 |
+
# ==============================================================================
|
| 48 |
+
# AS FUNÇÕES ABAIXO NÃO PRECISARAM DE MUDANÇAS, POIS A LÓGICA ACIMA RESOLVE TUDO!
|
| 49 |
+
# ==============================================================================
|
| 50 |
+
|
| 51 |
+
def adicionar_linha_sheets(historico):
|
| 52 |
+
cliente = conectar_sheets()
|
| 53 |
+
if not cliente:
|
| 54 |
+
return
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
print("1. Abrindo a planilha...")
|
| 58 |
+
# Abre a planilha pelo ID e seleciona a primeira aba (Página1)
|
| 59 |
+
planilha = cliente.open_by_key(ID_PLANILHA)
|
| 60 |
+
aba = planilha.sheet1
|
| 61 |
+
|
| 62 |
+
print("3. Preparando a nova linha...")
|
| 63 |
+
# Convertemos o dic em list pegamos seus valroes e enviamos como uma nova linha
|
| 64 |
+
linha_formato_lista = list(historico.values())
|
| 65 |
+
aba.append_row(linha_formato_lista)
|
| 66 |
+
|
| 67 |
+
aviso='1'
|
| 68 |
+
|
| 69 |
+
return aviso
|
| 70 |
+
|
| 71 |
+
except Exception as e:
|
| 72 |
+
aviso=e
|
| 73 |
+
return aviso
|
| 74 |
+
|
| 75 |
+
def ler_historico():
|
| 76 |
+
print("Baixando registros do Google Sheets...")
|
| 77 |
+
cliente = conectar_sheets()
|
| 78 |
+
planilha = cliente.open_by_key(ID_PLANILHA)
|
| 79 |
+
aba = planilha.sheet1
|
| 80 |
+
|
| 81 |
+
# O COMANDO MÁGICO AQUI:
|
| 82 |
+
dados = aba.get_all_records()
|
| 83 |
+
df = pd.DataFrame(dados)
|
| 84 |
+
return df
|
| 85 |
+
|
| 86 |
+
def senhas():
|
| 87 |
+
cliente = conectar_sheets()
|
| 88 |
+
planilha = cliente.open_by_key(ID_PLANILHA)
|
| 89 |
+
aba = planilha.worksheet("acessos")
|
| 90 |
+
|
| 91 |
+
# O COMANDO MÁGICO AQUI:
|
| 92 |
+
dados = aba.get_all_records()
|
| 93 |
+
df = pd.DataFrame(dados)
|
| 94 |
+
return df
|
Login.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import flet as ft
|
| 2 |
+
import Google_Sheets
|
| 3 |
+
def montar_tela_login(funcao_entrar_no_sistema):
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
texto_aviso = ft.TextField(
|
| 7 |
+
label=None,
|
| 8 |
+
width=0,
|
| 9 |
+
height=0,
|
| 10 |
+
border_radius=10,
|
| 11 |
+
border_color="white",
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# 1. Campos de Texto
|
| 17 |
+
campo_usuario = ft.TextField(
|
| 18 |
+
label="Usuário",
|
| 19 |
+
width=280,
|
| 20 |
+
height=50,
|
| 21 |
+
border_radius=10,
|
| 22 |
+
prefix_icon=ft.Icons.PERSON,
|
| 23 |
+
border_color="green",
|
| 24 |
+
focused_border_color="#2E7D32",
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
campo_senha = ft.TextField(
|
| 28 |
+
label="Senha",
|
| 29 |
+
width=280,
|
| 30 |
+
height=50,
|
| 31 |
+
border_radius=10,
|
| 32 |
+
password=True,
|
| 33 |
+
can_reveal_password=True,
|
| 34 |
+
prefix_icon=ft.Icons.LOCK,
|
| 35 |
+
border_color="green",
|
| 36 |
+
focused_border_color="#2E7D32",
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Chama a planilha para verificar o login e senha
|
| 40 |
+
retorno = Google_Sheets.senhas()
|
| 41 |
+
print(f'Retorno da planilha se senhas:{retorno}')
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# 3. A Lógica de Validação
|
| 45 |
+
def fazer_login(e):
|
| 46 |
+
user_digitado = (campo_usuario.value or "").strip()
|
| 47 |
+
senha_digitada = (campo_senha.value or "").strip()
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# Filtra os user e senhas do excel
|
| 51 |
+
tabela_filtrada = retorno[
|
| 52 |
+
(retorno['Usuário'].astype(str) == user_digitado) &
|
| 53 |
+
(retorno['Senha'].astype(str) == senha_digitada)
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Validação Básica: Deixou em branco?
|
| 61 |
+
if not user_digitado or not senha_digitada:
|
| 62 |
+
print("Preencha os campos!")
|
| 63 |
+
texto_aviso.value=f"❌ Preencha os campos! "
|
| 64 |
+
texto_aviso.bgcolor="yellow"
|
| 65 |
+
texto_aviso.border_color="black"
|
| 66 |
+
texto_aviso.height=50
|
| 67 |
+
texto_aviso.width=290
|
| 68 |
+
return
|
| 69 |
+
|
| 70 |
+
# Se o Pandas achou uma linha, ele entra! Se a tabela vier vazia, credencial inválida.
|
| 71 |
+
if not tabela_filtrada.empty:
|
| 72 |
+
print("Login com sucesso!")
|
| 73 |
+
|
| 74 |
+
e.page.session.store.set("user",user_digitado)
|
| 75 |
+
funcao_entrar_no_sistema(e) # Libera a passagem para o main.py!
|
| 76 |
+
else:
|
| 77 |
+
print("Tentativa de acesso negada.")
|
| 78 |
+
texto_aviso.value = f"❌ Acesso Incorreto ! "
|
| 79 |
+
texto_aviso.bgcolor = "orange"
|
| 80 |
+
texto_aviso.border_color = "black"
|
| 81 |
+
texto_aviso.height = 50
|
| 82 |
+
texto_aviso.width = 290
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# 2. Botão de Entrar
|
| 87 |
+
btn_entrar = ft.ElevatedButton(
|
| 88 |
+
content=ft.Text("ACESSAR SISTEMA", color="white"),
|
| 89 |
+
width=280,
|
| 90 |
+
height=40,
|
| 91 |
+
style=ft.ButtonStyle(
|
| 92 |
+
bgcolor="#2E7D32",
|
| 93 |
+
shape=ft.RoundedRectangleBorder(radius=10),
|
| 94 |
+
elevation=5,
|
| 95 |
+
),
|
| 96 |
+
on_click=fazer_login
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# 3. Definimos o Formulário Branco
|
| 100 |
+
formulario_branco = ft.Container(
|
| 101 |
+
content=ft.Column(
|
| 102 |
+
controls=[
|
| 103 |
+
# Colocando o texto de aviso na tela
|
| 104 |
+
texto_aviso,
|
| 105 |
+
|
| 106 |
+
ft.Divider(height=10, color="transparent"),
|
| 107 |
+
|
| 108 |
+
ft.Icon(ft.Icons.ACCOUNT_CIRCLE, size=80, color="#2E7D32"),
|
| 109 |
+
ft.Text("Bem-vindo", size=24, weight="bold", color="black"),
|
| 110 |
+
ft.Text("Faça login para continuar", size=14, color="grey"),
|
| 111 |
+
ft.Divider(height=20, color="transparent"),
|
| 112 |
+
|
| 113 |
+
campo_usuario,
|
| 114 |
+
ft.Divider(height=10, color="transparent"),
|
| 115 |
+
campo_senha,
|
| 116 |
+
ft.Divider(height=30, color="transparent"),
|
| 117 |
+
btn_entrar
|
| 118 |
+
],
|
| 119 |
+
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
| 120 |
+
spacing=5
|
| 121 |
+
),
|
| 122 |
+
width=380, # Aumentei um pouco para caber os inputs (280) + o padding (80)
|
| 123 |
+
height=500,
|
| 124 |
+
padding=40,
|
| 125 |
+
bgcolor="white",
|
| 126 |
+
border_radius=20,
|
| 127 |
+
shadow=ft.BoxShadow(
|
| 128 |
+
spread_radius=1,
|
| 129 |
+
blur_radius=15,
|
| 130 |
+
color=ft.Colors.BLACK45,
|
| 131 |
+
offset=ft.Offset(0, 0),
|
| 132 |
+
blur_style=ft.BlurStyle.OUTER,
|
| 133 |
+
),
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# ==============================================================================
|
| 137 |
+
# 4. LAYOUT FINAL (Totalmente Responsivo)
|
| 138 |
+
# ==============================================================================
|
| 139 |
+
layout = ft.Container(
|
| 140 |
+
# Coloca a imagem de fundo perfeitamente ajustada
|
| 141 |
+
image=ft.DecorationImage(
|
| 142 |
+
src="login fundo.webp",
|
| 143 |
+
fit="cover" # Faz a imagem cobrir toda a tela
|
| 144 |
+
),
|
| 145 |
+
expand=True, # Força o container a ocupar a tela inteira do navegador/monitor
|
| 146 |
+
|
| 147 |
+
# A MÁGICA ESTÁ AQUI: Alinha o conteúdo (o formulário) exatamente no centro de tudo!
|
| 148 |
+
alignment=ft.Alignment.CENTER,
|
| 149 |
+
|
| 150 |
+
content=formulario_branco
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
return layout
|
Qlik_sense.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
import requests # NOVO: Biblioteca nativa para requisições na web
|
| 4 |
+
from qlik_sdk import AuthType, Config, Apps
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import Google_Sheets
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# Abrindo o cofre.env para pegar as chaves
|
| 10 |
+
load_dotenv()
|
| 11 |
+
|
| 12 |
+
# --- 1. CONFIGURAÇÕES ---
|
| 13 |
+
TENANT_URL = os.getenv("TENANT_URL")
|
| 14 |
+
APP_ID = os.getenv("APP_ID")
|
| 15 |
+
CLIENT_ID = os.getenv("CLIENT_ID")
|
| 16 |
+
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
|
| 17 |
+
|
| 18 |
+
# --- 2. A AUTENTICAÇÃO BLINDADA (BUSCA MANUAL DO TOKEN) ---
|
| 19 |
+
print("Buscando autorização M2M no Qlik...")
|
| 20 |
+
|
| 21 |
+
# Fazemos o pedido oficial do Token direto na "porta" do Qlik Cloud
|
| 22 |
+
resposta = requests.post(
|
| 23 |
+
f"{TENANT_URL}/oauth/token",
|
| 24 |
+
json={
|
| 25 |
+
"client_id": CLIENT_ID,
|
| 26 |
+
"client_secret": CLIENT_SECRET,
|
| 27 |
+
"grant_type": "client_credentials"
|
| 28 |
+
},
|
| 29 |
+
headers={"Accept": "application/json", "Content-Type": "application/json"}
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
# Se der erro de senha/ID ou de permissão, ele avisa na hora
|
| 33 |
+
if resposta.status_code != 200:
|
| 34 |
+
print(f"❌ Erro na autenticação com o Qlik: {resposta.text}")
|
| 35 |
+
exit()
|
| 36 |
+
|
| 37 |
+
# Extraímos o código do token que acabamos de receber!
|
| 38 |
+
token_seguro = resposta.json().get("access_token")
|
| 39 |
+
print("✔️ Token gerado com sucesso!")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# O PULO DO GATO: Agora nós enganamos o SDK!
|
| 43 |
+
# Nós passamos o 'auth_type' como APIKey, e colocamos o token fresquinho ali dentro.
|
| 44 |
+
config = Config(
|
| 45 |
+
host=TENANT_URL,
|
| 46 |
+
auth_type=AuthType.APIKey,
|
| 47 |
+
api_key=token_seguro
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# Instanciamos a classe Apps normalmente. O SDK vai achar que é uma chave normal e abrir!
|
| 51 |
+
apps = Apps(config)
|
| 52 |
+
|
| 53 |
+
# =========================================================
|
| 54 |
+
|
| 55 |
+
def extrair_dados_engine():
|
| 56 |
+
print("Conectando ao App do Qlik...")
|
| 57 |
+
|
| 58 |
+
# Pega a referência do Aplicativo
|
| 59 |
+
app = apps.get(APP_ID)
|
| 60 |
+
try:
|
| 61 |
+
# Abre a sessão com a Engine (memória RAM do Qlik)
|
| 62 |
+
with app.open():
|
| 63 |
+
print(f"App '{app.attributes.name}' aberto! Montando a tabela virtual...")
|
| 64 |
+
|
| 65 |
+
# --- 3. DEFINA AS COLUNAS AQUI ---
|
| 66 |
+
campos_desejados = ["SAFRA","UF","CÓDIGO DO PRODUTO","DERIVAÇÃO","DESCRIÇÃO PRODUTO","MARCA","CUSTO ATUAL","DATA VENC ATUAL","DATA DE VENC NOVA","NOVO CUSTO","% BARTER","QUANTIDADE"]
|
| 67 |
+
|
| 68 |
+
limite_linhas = int(10000 / len(campos_desejados))
|
| 69 |
+
|
| 70 |
+
qDimensions = [{"qDef": {"qFieldDefs": [campo]}} for campo in campos_desejados]
|
| 71 |
+
|
| 72 |
+
properties = {
|
| 73 |
+
"qInfo": {"qType": "StraightTable"},
|
| 74 |
+
"qHyperCubeDef": {
|
| 75 |
+
"qDimensions": qDimensions,
|
| 76 |
+
"qInitialDataFetch": [{
|
| 77 |
+
"qTop": 0,
|
| 78 |
+
"qLeft": 0,
|
| 79 |
+
"qHeight": limite_linhas,
|
| 80 |
+
"qWidth": len(campos_desejados)
|
| 81 |
+
}]
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
# Cria o objeto na sessão e puxa o layout
|
| 86 |
+
obj = app.create_session_object(properties)
|
| 87 |
+
layout = obj.get_layout()
|
| 88 |
+
|
| 89 |
+
# Acessa a matriz de dados que a Engine devolveu
|
| 90 |
+
matriz_dados = layout.qHyperCube.qDataPages[0].qMatrix
|
| 91 |
+
|
| 92 |
+
# --- 4. CONVERTE PARA PANDAS ---
|
| 93 |
+
dados_limpos = []
|
| 94 |
+
for linha in matriz_dados:
|
| 95 |
+
registro = {}
|
| 96 |
+
for i, campo in enumerate(campos_desejados):
|
| 97 |
+
registro[campo] = linha[i].qText
|
| 98 |
+
dados_limpos.append(registro)
|
| 99 |
+
|
| 100 |
+
df = pd.DataFrame(dados_limpos)
|
| 101 |
+
|
| 102 |
+
#Crio uma nova coluna onde add a opção de salvar o registro
|
| 103 |
+
df["Salvar alteração"]=""
|
| 104 |
+
|
| 105 |
+
#Chamando a leitura do google sheets para colocar os valores na tela
|
| 106 |
+
hist=Google_Sheets.ler_historico()
|
| 107 |
+
|
| 108 |
+
# 2. Só faz a mágica se tiver dados no Sheets
|
| 109 |
+
if not hist.empty:
|
| 110 |
+
print("Cruzando dados do Qlik com o Google Sheets...")
|
| 111 |
+
|
| 112 |
+
# Passo A: Transforma a coluna de texto em DATA DE VERDADE para o Python saber quem é mais novo
|
| 113 |
+
hist['DATA REGISTRADA'] = pd.to_datetime(hist['DATA REGISTRADA'], format="%d/%m/%Y %H:%M:%S")
|
| 114 |
+
|
| 115 |
+
#Vou forçar as 2 colunas de produto tanto do excel quanto do qlik a serem textos sem espaços
|
| 116 |
+
hist['PRODUTO']=hist['PRODUTO'].astype(str).str.strip()
|
| 117 |
+
df['CÓDIGO DO PRODUTO']=df['CÓDIGO DO PRODUTO'].astype(str).str.strip()
|
| 118 |
+
|
| 119 |
+
# Passo B: Ordena do mais novo pro mais velho, e remove produtos repetidos (fica só o último salvo!)
|
| 120 |
+
hist_recente = hist.sort_values(by='DATA REGISTRADA', ascending=False).drop_duplicates(subset=['PRODUTO'],keep='first')
|
| 121 |
+
|
| 122 |
+
# Passo C: Transforma o 'Cod Produto' na chave de busca (como se fosse a 1ª coluna do PROCV no Excel)
|
| 123 |
+
hist_recente.set_index('PRODUTO', inplace=True)
|
| 124 |
+
|
| 125 |
+
# Passo D: Faz o PROCV mapeando o Código do Produto do Qlik com o do Sheets
|
| 126 |
+
# Se não achar nada (.fillna("")), deixa a caixinha em branco ("")
|
| 127 |
+
df["DATA DE VENC NOVA"] = df['CÓDIGO DO PRODUTO'].map(hist_recente['DATA VENC NOVA']).fillna("")
|
| 128 |
+
df["NOVO CUSTO"] = df['CÓDIGO DO PRODUTO'].map(hist_recente['NOVO CUSTO']).fillna("")
|
| 129 |
+
df["% BARTER"] = df['CÓDIGO DO PRODUTO'].map(hist_recente['BARTER %']).fillna("")
|
| 130 |
+
df["QUANTIDADE"] = df['CÓDIGO DO PRODUTO'].map(hist_recente['QUANTIDADE']).fillna("")
|
| 131 |
+
|
| 132 |
+
# 3. Se a planilha do Sheets estiver totalmente vazia (primeiro dia de uso)
|
| 133 |
+
else:
|
| 134 |
+
print("Planilha vazia. Criando colunas em branco.")
|
| 135 |
+
df["DATA DE VENC NOVA"] = ""
|
| 136 |
+
df["NOVO CUSTO"] = ""
|
| 137 |
+
df["% BARTER"] = ""
|
| 138 |
+
df["QUANTIDADE"] = ""
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
print(f"Sucesso! Extraídas {df} linhas do Qlik.")
|
| 142 |
+
return df,hist
|
| 143 |
+
|
| 144 |
+
except Exception as e:
|
| 145 |
+
print(f"Erro ao extrair dados do Qlik: {e}")
|
| 146 |
+
return None
|
| 147 |
+
|
Registros.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import flet as ft
|
| 2 |
+
import Google_Sheets
|
| 3 |
+
|
| 4 |
+
#Cria uma variavel global para atualziar a base quando atualizada
|
| 5 |
+
upd=None
|
| 6 |
+
|
| 7 |
+
def montar_tela_registros(loogof,retorno,base):
|
| 8 |
+
print('Iniciando modulo registros (Lendo direto do Sheets)')
|
| 9 |
+
|
| 10 |
+
#Recebe a base lida no inicio do código
|
| 11 |
+
global upd
|
| 12 |
+
|
| 13 |
+
upd=base
|
| 14 |
+
|
| 15 |
+
#Logica para retroanr a tela de login
|
| 16 |
+
def ir_para_login(e=None):
|
| 17 |
+
|
| 18 |
+
loogof()
|
| 19 |
+
|
| 20 |
+
#Função para voltar a tela principal
|
| 21 |
+
def tela_principal(e=None):
|
| 22 |
+
|
| 23 |
+
retorno()
|
| 24 |
+
|
| 25 |
+
#Função para atualizar a tela de registros
|
| 26 |
+
def atualizar(e):
|
| 27 |
+
global upd
|
| 28 |
+
upd = Google_Sheets.ler_historico()
|
| 29 |
+
|
| 30 |
+
preencher_tabela(upd)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# 2. SE A PLANILHA ESTIVER VAZIA
|
| 34 |
+
if upd.empty:
|
| 35 |
+
conteudo_tabela = ft.Text("Nenhuma alteração realizada.", color="black", size=20)
|
| 36 |
+
|
| 37 |
+
# 3. SE TIVER DADOS, MONTA A TABELA
|
| 38 |
+
else:
|
| 39 |
+
print('Lendo os registros e montando a tela...')
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# Pega os nomes das colunas da Linha 1 do Sheets
|
| 43 |
+
nomes_colunas = upd.columns.tolist()
|
| 44 |
+
colunas_flet = [ft.DataColumn(ft.Text(str(nome), weight="bold", color="black")) for nome in nomes_colunas]
|
| 45 |
+
|
| 46 |
+
tabela_obj = ft.DataTable(
|
| 47 |
+
columns=colunas_flet,
|
| 48 |
+
rows=[], # Deixamos vazio para a função abaixo preencher
|
| 49 |
+
heading_row_color="#C8E6C9",
|
| 50 |
+
column_spacing=20,
|
| 51 |
+
data_row_min_height=40,
|
| 52 |
+
)
|
| 53 |
+
conteudo_tabela = ft.Column([tabela_obj], scroll=ft.ScrollMode.AUTO, expand=True)
|
| 54 |
+
|
| 55 |
+
# --- FUNÇÃO DE PREENCHER A TABELA (TOTALMENTE CONVERTIDA PARA PANDAS) ---
|
| 56 |
+
def preencher_tabela(dataframe,precisa_atualizar=True):
|
| 57 |
+
linhas_flet = []
|
| 58 |
+
|
| 59 |
+
# O jeito certo de ler um DataFrame linha a linha:
|
| 60 |
+
for index, row in dataframe.iterrows():
|
| 61 |
+
celulas = []
|
| 62 |
+
|
| 63 |
+
# Para cada nome de coluna, pega o valor exato daquela célula
|
| 64 |
+
for col in nomes_colunas:
|
| 65 |
+
valor = row[col]
|
| 66 |
+
celulas.append(ft.DataCell(ft.Text(str(valor), color="black", size=12)))
|
| 67 |
+
|
| 68 |
+
linhas_flet.append(ft.DataRow(cells=celulas,
|
| 69 |
+
|
| 70 |
+
#Criamos uma configuração de cores para ele ter o visual de houver na tela
|
| 71 |
+
color={
|
| 72 |
+
#Cor quando ele passa o moouse por cima
|
| 73 |
+
ft.ControlState.HOVERED:"#a7d498",
|
| 74 |
+
|
| 75 |
+
# Cor para o exato momento do clique
|
| 76 |
+
ft.ControlState.PRESSED: "#a7d498",
|
| 77 |
+
|
| 78 |
+
# Cor de quando você está com o cursor piscando lá dentro digitando
|
| 79 |
+
ft.ControlState.FOCUSED: "#a7d498",
|
| 80 |
+
|
| 81 |
+
# Outra cor para quando ele clica
|
| 82 |
+
ft.ControlState.SELECTED:"#a7d498",
|
| 83 |
+
|
| 84 |
+
#Outra cor para quando ele não passa o mouse nem clica
|
| 85 |
+
ft.ControlState.DEFAULT:ft.Colors.TRANSPARENT,
|
| 86 |
+
|
| 87 |
+
},
|
| 88 |
+
#Essa função serve apenas para avisar ao flet que essa linha deve ser ativa, assim o hover funciona
|
| 89 |
+
on_select_change=lambda e: print('Acordando a linha')
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
))
|
| 93 |
+
tabela_obj.rows = linhas_flet
|
| 94 |
+
|
| 95 |
+
if precisa_atualizar:
|
| 96 |
+
tabela_obj.update()
|
| 97 |
+
conteudo_tabela.update()
|
| 98 |
+
|
| 99 |
+
# --- A SUPER BUSCA GLOBAL (ESTILO GOOGLE) ---
|
| 100 |
+
|
| 101 |
+
def filtrar_tudo(e):
|
| 102 |
+
|
| 103 |
+
print('iniciando filtro')
|
| 104 |
+
# 1. Pega o texto, deixa minúsculo e tira espaços sobrando nas pontas
|
| 105 |
+
texto = e.control.value.lower().strip()
|
| 106 |
+
|
| 107 |
+
# 2. Se apagou tudo, devolve a tabela original
|
| 108 |
+
if texto == "" or texto == " ":
|
| 109 |
+
preencher_tabela(upd.head(50))
|
| 110 |
+
print('sem dados para filtrar')
|
| 111 |
+
return
|
| 112 |
+
|
| 113 |
+
# 3. O SEGREDO: Corta o texto nos espaços!
|
| 114 |
+
# Se ele digitou "AGRO 3000", vira uma lista: ["agro", "3000"]
|
| 115 |
+
palavras_digitadas = texto.split()
|
| 116 |
+
|
| 117 |
+
# Fazemos uma cópia da tabela inteira para ir "espremendo" ela
|
| 118 |
+
df_filtrado = upd.copy()
|
| 119 |
+
|
| 120 |
+
# 4. O Loop do Afunilamento
|
| 121 |
+
for palavra in palavras_digitadas:
|
| 122 |
+
print(f'Filtrando as palavras:{palavra}')
|
| 123 |
+
# Para CADA palavra, ele procura na tabela e joga fora o que não bater
|
| 124 |
+
mascara_busca = df_filtrado.astype(str).apply(
|
| 125 |
+
lambda col: col.str.lower().str.contains(palavra, na=False))
|
| 126 |
+
df_filtrado = df_filtrado[mascara_busca.any(axis=1)]
|
| 127 |
+
|
| 128 |
+
# No final do loop, só sobram as linhas que têm TODAS as palavras digitadas!
|
| 129 |
+
preencher_tabela(df_filtrado.head(50))
|
| 130 |
+
|
| 131 |
+
# Campo de busca inteligente
|
| 132 |
+
campo_busca = ft.TextField(
|
| 133 |
+
label="Filtrar qualquer campo...",
|
| 134 |
+
prefix_icon=ft.Icons.SEARCH,
|
| 135 |
+
width=250,
|
| 136 |
+
height=40,
|
| 137 |
+
text_size=14,
|
| 138 |
+
border_color="white24",
|
| 139 |
+
focused_border_color="green",
|
| 140 |
+
label_style=ft.TextStyle(color="white70"),
|
| 141 |
+
on_submit=filtrar_tudo # Chama a função quando clicamos no enter
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# ==============================================================================
|
| 145 |
+
# --- O CABEÇALHO RESPONSIVO (Mesma lógica da tela principal) ---
|
| 146 |
+
# ==============================================================================
|
| 147 |
+
linha_cabecalho = ft.Row(
|
| 148 |
+
controls=[
|
| 149 |
+
# 1. TEXTO DA ESQUERDA
|
| 150 |
+
ft.Text("App Custo de Produtos - Setor Compras", color="white", weight="bold", size=18),
|
| 151 |
+
|
| 152 |
+
# 2. PRIMEIRA MOLA: Empurra o centro
|
| 153 |
+
ft.Container(expand=True),
|
| 154 |
+
|
| 155 |
+
# 3. O "PACOTE" CENTRAL: Busca, Botões e Linhas Divisórias
|
| 156 |
+
ft.Row(
|
| 157 |
+
controls=[
|
| 158 |
+
campo_busca,
|
| 159 |
+
|
| 160 |
+
# Linha Divisória 1
|
| 161 |
+
ft.Container(width=1, height=30, bgcolor="white54", margin=ft.margin.symmetric(horizontal=10)),
|
| 162 |
+
|
| 163 |
+
ft.TextButton("Lista De Produtos", on_click=tela_principal, style=ft.ButtonStyle(
|
| 164 |
+
color={ft.ControlState.DEFAULT: ft.Colors.WHITE,
|
| 165 |
+
ft.ControlState.HOVERED: ft.Colors.BLACK})),
|
| 166 |
+
|
| 167 |
+
# Linha Divisória 2
|
| 168 |
+
ft.Container(width=1, height=30, bgcolor="white54", margin=ft.margin.symmetric(horizontal=10)),
|
| 169 |
+
|
| 170 |
+
ft.TextButton("Meus Registros", style=ft.ButtonStyle(
|
| 171 |
+
color={ft.ControlState.DEFAULT: ft.Colors.WHITE,
|
| 172 |
+
ft.ControlState.HOVERED: ft.Colors.BLACK})),
|
| 173 |
+
],
|
| 174 |
+
spacing=5,
|
| 175 |
+
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
| 176 |
+
),
|
| 177 |
+
|
| 178 |
+
# 4. SEGUNDA MOLA: Empurra os botões de ação lá pro canto direito
|
| 179 |
+
ft.Container(expand=True),
|
| 180 |
+
|
| 181 |
+
# 5. PACOTE DA DIREITA: Agrupa o botão Atualizar e o Sair lado a lado
|
| 182 |
+
ft.Row(
|
| 183 |
+
controls=[
|
| 184 |
+
# Botão Atualizar (Azul)
|
| 185 |
+
ft.Container(
|
| 186 |
+
content=ft.Icon(icon=ft.Icons.UPDATE_ROUNDED, color="white", size=20),
|
| 187 |
+
on_click=atualizar,
|
| 188 |
+
tooltip="Atualizar a tela!",
|
| 189 |
+
bgcolor="blue", width=35, height=35, border_radius=5, alignment=ft.Alignment.CENTER,
|
| 190 |
+
ink=True
|
| 191 |
+
),
|
| 192 |
+
|
| 193 |
+
# Botão Sair (Vermelho)
|
| 194 |
+
ft.Container(
|
| 195 |
+
content=ft.Icon(icon=ft.Icons.POWER_SETTINGS_NEW, color="white", size=20),
|
| 196 |
+
on_click=ir_para_login,
|
| 197 |
+
tooltip="Sair do Sistema!",
|
| 198 |
+
bgcolor="red", width=35, height=35, border_radius=5, alignment=ft.Alignment.CENTER, ink=True
|
| 199 |
+
)
|
| 200 |
+
],
|
| 201 |
+
spacing=10 # Pequeno espaço entre o botão azul e o vermelho
|
| 202 |
+
)
|
| 203 |
+
],
|
| 204 |
+
alignment=ft.MainAxisAlignment.START,
|
| 205 |
+
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
# ==============================================================================
|
| 209 |
+
# --- A ÁREA DA TABELA RESPONSIVA ---
|
| 210 |
+
# ==============================================================================
|
| 211 |
+
area_tabela = ft.Container(
|
| 212 |
+
content=ft.Column(
|
| 213 |
+
controls=[
|
| 214 |
+
ft.Text("Histórico de Alterações", size=25, weight="bold", color="black"),
|
| 215 |
+
ft.Divider(),
|
| 216 |
+
conteudo_tabela # Aqui entra a tabela que foi montada lá em cima
|
| 217 |
+
],
|
| 218 |
+
expand=True # Garante que a coluna ocupe tudo
|
| 219 |
+
),
|
| 220 |
+
bgcolor="white",
|
| 221 |
+
border_radius=10,
|
| 222 |
+
padding=10,
|
| 223 |
+
expand=True # Faz a caixa branca esticar até o fim da tela
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
# ==============================================================================
|
| 227 |
+
# --- O LAYOUT PRINCIPAL DA TELA ---
|
| 228 |
+
# ==============================================================================
|
| 229 |
+
layout = ft.Container(
|
| 230 |
+
image=ft.DecorationImage(
|
| 231 |
+
src="BP - Analítico-App-Custo-v2 (1).png",
|
| 232 |
+
fit="cover"
|
| 233 |
+
),
|
| 234 |
+
expand=True,
|
| 235 |
+
padding=ft.Padding(top=20, left=30, right=30, bottom=20),
|
| 236 |
+
content=ft.Column(
|
| 237 |
+
controls=[
|
| 238 |
+
linha_cabecalho,
|
| 239 |
+
area_tabela
|
| 240 |
+
],
|
| 241 |
+
expand=True,
|
| 242 |
+
spacing=20
|
| 243 |
+
)
|
| 244 |
+
)
|
| 245 |
+
# Preenche a tabela com todos os dados logo que a tela carregar
|
| 246 |
+
if not upd.empty:
|
| 247 |
+
# Passamos False para ele NÃO tentar desenhar antes da hora!
|
| 248 |
+
preencher_tabela(upd, precisa_atualizar=False)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
return layout
|
login fundo.webp
ADDED
|
main.py
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#Funções de bibliotecas exportadas
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
+
#Bibliotecas interas
|
| 5 |
+
import flet as ft
|
| 6 |
+
import asyncio
|
| 7 |
+
|
| 8 |
+
#Importa modulos e suas funções
|
| 9 |
+
import Registros
|
| 10 |
+
import Login
|
| 11 |
+
import Qlik_sense
|
| 12 |
+
import Google_Sheets
|
| 13 |
+
|
| 14 |
+
#Var global para receber os dados do qlik
|
| 15 |
+
df=None
|
| 16 |
+
|
| 17 |
+
#Var de global para buscar o historico
|
| 18 |
+
df_hist=None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
#Função mae, na qual chamamos as demais e oraganizamos a logica
|
| 23 |
+
def main(page: ft.Page):
|
| 24 |
+
|
| 25 |
+
#Definindo tamanho e parametos da pagina, nome etc
|
| 26 |
+
page.padding = 0
|
| 27 |
+
page.spacing = 0
|
| 28 |
+
page.bgcolor = ft.Colors.BLACK
|
| 29 |
+
page.window_maximized = True
|
| 30 |
+
page.title = "Sistema de Precificação"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# 2. FUNÇÃO QUE RODA ESCONDIDA NO FUNDO
|
| 34 |
+
async def carregar_no_fundo():
|
| 35 |
+
global df, df_hist
|
| 36 |
+
|
| 37 |
+
await asyncio.sleep(2)
|
| 38 |
+
print("Iniciando download silencioso do Qlik... e sheets")
|
| 39 |
+
df, df_hist = Qlik_sense.extrair_dados_engine()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
print("Download do Qlik e sheets concluído com sucesso!")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# 3. TELA DE LOGIN
|
| 47 |
+
def ir_para_login(e=None):
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
page.clean()
|
| 51 |
+
tela = Login.montar_tela_login(ir_para_principal)
|
| 52 |
+
page.add(tela)
|
| 53 |
+
page.update()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
#Elimino toda a configuração atual de espaçamento da tela para eu poder configurar como desejar abaixo com flet
|
| 57 |
+
page.padding=0
|
| 58 |
+
page.spacing=0
|
| 59 |
+
page.expand=True
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# --- FUNÇÃO 1: CONSTRÓI A TELA DE REGISTROS ---
|
| 66 |
+
def ir_para_registros(e):
|
| 67 |
+
page.clean() # 1. Limpa a tela principal
|
| 68 |
+
global df_hist
|
| 69 |
+
print('Iniciado outro modulo')
|
| 70 |
+
|
| 71 |
+
# 2. Chama o módulo para desenhar a tela nova
|
| 72 |
+
# Passamos a função 'ir_para_principal' para ele saber como voltar
|
| 73 |
+
tela = Registros.montar_tela_registros(ir_para_login,ir_para_principal,df_hist)
|
| 74 |
+
|
| 75 |
+
page.add(tela) # 3. Adiciona a tela nova
|
| 76 |
+
page.update()
|
| 77 |
+
|
| 78 |
+
# Dic que define tamanho das colunas
|
| 79 |
+
larguras_colunas = {
|
| 80 |
+
"SAFRA":100,
|
| 81 |
+
"CÓDIGO DO PRODUTO": 150,
|
| 82 |
+
"DERIVAÇÃO":80,
|
| 83 |
+
"DESCRIÇÃO PRODUTO": 200,
|
| 84 |
+
"MARCA": 180,
|
| 85 |
+
"CUSTO ATUAL": 120, # Maior porque tem muito texto
|
| 86 |
+
"DATA VENC ATUAL": 180,
|
| 87 |
+
"DATA DE VENC NOVA":180,
|
| 88 |
+
"NOVO CUSTO":100,
|
| 89 |
+
"% BARTER":100,
|
| 90 |
+
"QUANTIDADE":100,
|
| 91 |
+
"Salvar alteração":100
|
| 92 |
+
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
def get_largura(nome_coluna):
|
| 96 |
+
return larguras_colunas.get(nome_coluna, 120)
|
| 97 |
+
|
| 98 |
+
#Essa def cria o esqueleto da pagina com a informações retornadas
|
| 99 |
+
def ir_para_principal(e=None):
|
| 100 |
+
page.clean()
|
| 101 |
+
print('Iniciando tela principal')
|
| 102 |
+
|
| 103 |
+
#Uso a var globar para extrari o retorno do qlik
|
| 104 |
+
global df
|
| 105 |
+
|
| 106 |
+
# Criamos a tabela de dados como um objeto global dentro do main para podermos atualizar depois
|
| 107 |
+
tabela_dados_obj = ft.DataTable(
|
| 108 |
+
columns=[ft.DataColumn(ft.Text("")) for _ in df.columns],
|
| 109 |
+
rows=[], # Começa vazia, vamos preencher na função
|
| 110 |
+
column_spacing=50,
|
| 111 |
+
data_row_max_height=float("inf"),
|
| 112 |
+
data_row_min_height=60,
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
#Empacoto a tabela acima que tem só colunas em uma coluna do flet
|
| 119 |
+
container_colum = ft.Column(
|
| 120 |
+
controls=[tabela_dados_obj], # Apenas a tabela diretamente!
|
| 121 |
+
scroll=ft.ScrollMode.AUTO,
|
| 122 |
+
expand=True,
|
| 123 |
+
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
# Cabeçalho Fixo (seu código atual) AQUI COLOCAMOS O CABEÇALHO EM NEGRITO COM LETRA PRETA
|
| 128 |
+
colunas_cabecalho = [
|
| 129 |
+
ft.DataColumn(
|
| 130 |
+
ft.Container(
|
| 131 |
+
content=ft.Text(col, weight="bold", color="black"),
|
| 132 |
+
width=get_largura(col),
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
)
|
| 137 |
+
) for col in df.columns
|
| 138 |
+
]
|
| 139 |
+
|
| 140 |
+
#AQUI LEMOS O CABEÇALHO ACIMA COLOCAMOS UM FUNDO TRAPARENTE E UM TAMANHO E ALTURA
|
| 141 |
+
tabela_cabecalho = ft.DataTable(
|
| 142 |
+
|
| 143 |
+
columns=colunas_cabecalho,
|
| 144 |
+
|
| 145 |
+
heading_row_color=ft.Colors.TRANSPARENT,
|
| 146 |
+
column_spacing=50,
|
| 147 |
+
heading_row_height=50,
|
| 148 |
+
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
# Colunas editavel listadas para usarmos em outras defs
|
| 152 |
+
COLUNA_EDITAVEL = ["DATA DE VENC NOVA","NOVO CUSTO","QUANTIDADE","% BARTER"]
|
| 153 |
+
|
| 154 |
+
#Cria uma def para fechar nosso banner imporovisado
|
| 155 |
+
def fechar_banner_custom(e):
|
| 156 |
+
banner_customizado.visible = False
|
| 157 |
+
page.update()
|
| 158 |
+
|
| 159 |
+
#Cria um campo de texto vazio
|
| 160 |
+
texto_banner = ft.Text("", color="black", size=16, weight="bold")
|
| 161 |
+
|
| 162 |
+
#Cria nosso banner e recebe o texto vazio
|
| 163 |
+
banner_customizado = ft.Container(
|
| 164 |
+
content=ft.Row(
|
| 165 |
+
controls=[
|
| 166 |
+
texto_banner,
|
| 167 |
+
ft.Container(expand=True),
|
| 168 |
+
ft.TextButton("OK, ENTENDI", on_click=fechar_banner_custom, style=ft.ButtonStyle(color="black"))
|
| 169 |
+
]
|
| 170 |
+
),
|
| 171 |
+
bgcolor="green",
|
| 172 |
+
top=0, left=0, right=0,
|
| 173 |
+
padding=15,
|
| 174 |
+
visible=False,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
# --- FUNÇÃO QUE SALVA A LINHA TODA ---
|
| 180 |
+
def salvar_edicao(df_index, caixas_da_linha):
|
| 181 |
+
|
| 182 |
+
print("Iniciando verificação da linha completa...")
|
| 183 |
+
|
| 184 |
+
valores_validados = {} # Vamos guardar aqui os valores que passarem no teste
|
| 185 |
+
|
| 186 |
+
# 1. VALIDAÇÃO: Vamos olhar cada caixinha daquela linha
|
| 187 |
+
for nome_coluna, campo_texto in caixas_da_linha.items():
|
| 188 |
+
valor_digitado = campo_texto.value
|
| 189 |
+
|
| 190 |
+
# Se o campo estiver vazio, pula e não altera nada
|
| 191 |
+
if valor_digitado == "" or valor_digitado == "-":
|
| 192 |
+
continue
|
| 193 |
+
|
| 194 |
+
# Testa os números
|
| 195 |
+
if nome_coluna in ["NOVO CUSTO", "% BARTER", "QUANTIDADE"]:
|
| 196 |
+
try:
|
| 197 |
+
valor_limpo = valor_digitado.replace('.', '').replace(',', '.')
|
| 198 |
+
valores_validados[nome_coluna] = float(valor_limpo)
|
| 199 |
+
print('Salvo valor numerico com sucesso')
|
| 200 |
+
except ValueError:
|
| 201 |
+
print('deu erro na hora de validar campos de valor')
|
| 202 |
+
|
| 203 |
+
banner_customizado.bgcolor = "orange"
|
| 204 |
+
texto_banner.value = f" ❌ O campo NOVO CUSTO, % BARTER, QUANTIDADE , precisa ser valor númerico valido "
|
| 205 |
+
banner_customizado.visible = True
|
| 206 |
+
page.update()
|
| 207 |
+
return # Aborta o salvamento da linha inteira!
|
| 208 |
+
|
| 209 |
+
# Testa as datas
|
| 210 |
+
elif nome_coluna in ["DATA DE VENC NOVA"]:
|
| 211 |
+
try:
|
| 212 |
+
datetime.strptime(valor_digitado, "%d/%m/%Y")
|
| 213 |
+
valores_validados[nome_coluna] = valor_digitado
|
| 214 |
+
|
| 215 |
+
except ValueError:
|
| 216 |
+
|
| 217 |
+
print('Deu erro no momento de validar data')
|
| 218 |
+
|
| 219 |
+
banner_customizado.bgcolor = "orange"
|
| 220 |
+
texto_banner.value = f" ❌ O campo DATA DE VENC NOVA , precisa ser uma data valida "
|
| 221 |
+
banner_customizado.visible = True
|
| 222 |
+
page.update()
|
| 223 |
+
return # Aborta o salvamento da linha inteira!
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
# 2. SE PASSOU EM TUDO SEM ERROS, ATUALIZA O PANDAS
|
| 227 |
+
for nome_coluna, valor_novo in valores_validados.items():
|
| 228 |
+
if df[nome_coluna].dtype == 'string' or df[nome_coluna].dtype.name == 'string':
|
| 229 |
+
df[nome_coluna] = df[nome_coluna].astype('object')
|
| 230 |
+
|
| 231 |
+
df.at[df_index, nome_coluna] = valor_novo
|
| 232 |
+
print(f'Resposta salva no df:{df[nome_coluna]}')
|
| 233 |
+
|
| 234 |
+
# 3. Tira a foto da linha atualizada com TUDO o que foi digitado
|
| 235 |
+
linha_atualizada = df.loc[df_index]
|
| 236 |
+
|
| 237 |
+
# 4. Monta o Dicionário mestre para o Google Sheets
|
| 238 |
+
data_hora = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
| 239 |
+
registro = {
|
| 240 |
+
"Data/Hora": data_hora,
|
| 241 |
+
"User": page.session.store.get("user") or "Desconhecido",
|
| 242 |
+
"Safra": linha_atualizada['SAFRA'],
|
| 243 |
+
"Produto": linha_atualizada['CÓDIGO DO PRODUTO'],
|
| 244 |
+
"Derv": linha_atualizada['DERIVAÇÃO'],
|
| 245 |
+
"desc prod": linha_atualizada['DESCRIÇÃO PRODUTO'],
|
| 246 |
+
"Marca": linha_atualizada['MARCA'],
|
| 247 |
+
"Nv custo": linha_atualizada['CUSTO ATUAL'],
|
| 248 |
+
"Data venc atual": linha_atualizada['DATA VENC ATUAL'],
|
| 249 |
+
"Data de venc nv": linha_atualizada['DATA DE VENC NOVA'],
|
| 250 |
+
"Novo custo": linha_atualizada['NOVO CUSTO'],
|
| 251 |
+
"Barter": linha_atualizada['% BARTER'],
|
| 252 |
+
"qtd": linha_atualizada['QUANTIDADE'],
|
| 253 |
+
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
# 5. Envia para o Google Sheets UMA única vez
|
| 258 |
+
retorno=Google_Sheets.adicionar_linha_sheets(registro)
|
| 259 |
+
print(f'Retorno do mod sheets {retorno}')
|
| 260 |
+
if retorno =='1':
|
| 261 |
+
print('Tentando atualizar tela sucesso')
|
| 262 |
+
|
| 263 |
+
banner_customizado.bgcolor = "white"
|
| 264 |
+
texto_banner.value = "✔️ Registro salvo com sucesso 💾!"
|
| 265 |
+
banner_customizado.visible = True
|
| 266 |
+
page.update()
|
| 267 |
+
|
| 268 |
+
else:
|
| 269 |
+
|
| 270 |
+
print('Tentando atualizar tela com erro (Banner)')
|
| 271 |
+
|
| 272 |
+
banner_customizado.bgcolor = "orange"
|
| 273 |
+
texto_banner.value = f"❌ Ops! Ocorreu um erro: {retorno}"
|
| 274 |
+
banner_customizado.visible = True
|
| 275 |
+
page.update()
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# --- FUNÇÃO DE PREENCHER A TABELA (MODIFICADA) ELA QUE VAI SEMPRE ATUALZIAR A TABELA COM OS DADOS RETORNADOS DO QLIK E FILTROS ---
|
| 280 |
+
def preencher_tabela(dataframe):
|
| 281 |
+
linhas = []
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
# iterrows() agora nos dá o 'index' real da linha no DataFrame (fundamental para atualizar o dado correto)
|
| 285 |
+
for index, row in dataframe.iterrows():
|
| 286 |
+
celulas = []
|
| 287 |
+
|
| 288 |
+
# um dic para guardar al alterações
|
| 289 |
+
cx_tx_linha = {}
|
| 290 |
+
|
| 291 |
+
for col_name in dataframe.columns:
|
| 292 |
+
valor = row[col_name]
|
| 293 |
+
if col_name == 'CÓDIGO DO PRODUTO':
|
| 294 |
+
texto_formatado=str(valor)
|
| 295 |
+
|
| 296 |
+
else:
|
| 297 |
+
texto_formatado = f"{valor:.2f}" if isinstance(valor, (int, float)) else str(valor)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
#Verifico se estou olhando para a coluna botão
|
| 301 |
+
if col_name == "Salvar alteração":
|
| 302 |
+
|
| 303 |
+
botao_salvar= ft.IconButton(
|
| 304 |
+
icon=ft.Icons.SAVE,
|
| 305 |
+
icon_color="green",
|
| 306 |
+
tooltip="Salvar registro",
|
| 307 |
+
|
| 308 |
+
# Aqui damos o poder par ao botão enviar para a def que mnd para o sheets
|
| 309 |
+
on_click=lambda e, idx=index, cx=cx_tx_linha: salvar_edicao(idx, cx)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
)
|
| 314 |
+
celulas.append(ft.DataCell(ft.Container(content=botao_salvar, width=get_largura(col_name))))
|
| 315 |
+
# SE FOR A COLUNA EDITÁVEL, COLOCA UM TEXTFIELD
|
| 316 |
+
elif col_name in COLUNA_EDITAVEL:
|
| 317 |
+
campo_editavel = ft.TextField(
|
| 318 |
+
value=texto_formatado,
|
| 319 |
+
text_size=12,
|
| 320 |
+
height=35,
|
| 321 |
+
content_padding=5,
|
| 322 |
+
border_color="transparent", # Deixa invisível até focar
|
| 323 |
+
focused_border_color="blue",
|
| 324 |
+
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
# Guarda essa caixinha no nosso dicionário para o botão ler depois!
|
| 328 |
+
cx_tx_linha[col_name] = campo_editavel
|
| 329 |
+
|
| 330 |
+
celulas.append(ft.DataCell(ft.Container(content=campo_editavel,width=get_largura(col_name))))
|
| 331 |
+
|
| 332 |
+
# SE FOR OUTRA COLUNA, COLOCA TEXTO NORMAL (Bloqueado)
|
| 333 |
+
else:
|
| 334 |
+
texto_visual = ft.Text(texto_formatado, color="black", size=12)
|
| 335 |
+
# Envolvemos no Container
|
| 336 |
+
celulas.append(ft.DataCell(ft.Container(content=texto_visual, width=get_largura(col_name), padding=ft.Padding.symmetric(vertical=10, horizontal=0))))
|
| 337 |
+
|
| 338 |
+
#Adicionamos os valores na lsita declarada no topo da def adicionams um hover nele
|
| 339 |
+
linhas.append(ft.DataRow(cells=celulas,
|
| 340 |
+
|
| 341 |
+
#Criamos uma configuração de cores para ele ter o visual de houver na tela
|
| 342 |
+
color={
|
| 343 |
+
#Cor quando ele passa o moouse por cima
|
| 344 |
+
ft.ControlState.HOVERED:"#a7d498",
|
| 345 |
+
|
| 346 |
+
# Cor para o exato momento do clique
|
| 347 |
+
ft.ControlState.PRESSED: "#a7d498",
|
| 348 |
+
|
| 349 |
+
# Cor de quando você está com o cursor piscando lá dentro digitando
|
| 350 |
+
ft.ControlState.FOCUSED: "#a7d498",
|
| 351 |
+
|
| 352 |
+
# Outra cor para quando ele clica
|
| 353 |
+
ft.ControlState.SELECTED:"#a7d498",
|
| 354 |
+
|
| 355 |
+
#Outra cor para quando ele não passa o mouse nem clica
|
| 356 |
+
ft.ControlState.DEFAULT:ft.Colors.TRANSPARENT,
|
| 357 |
+
|
| 358 |
+
},
|
| 359 |
+
#Essa função serve apenas para avisar ao flet que essa linha deve ser ativa, assim o hover funciona
|
| 360 |
+
on_select_change=lambda e: print('Acordando a linha')
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
))
|
| 364 |
+
|
| 365 |
+
#Add na tabelas essas linhs configuradas
|
| 366 |
+
tabela_dados_obj.rows = linhas
|
| 367 |
+
tabela_dados_obj.update()
|
| 368 |
+
|
| 369 |
+
# 2. NOVO: Força a barra de rolagem a recalcular o tamanho correto!
|
| 370 |
+
container_colum.update()
|
| 371 |
+
page.update()
|
| 372 |
+
|
| 373 |
+
# --- A SUPER BUSCA GLOBAL (ESTILO GOOGLE) ---
|
| 374 |
+
def filtrar_tudo(e):
|
| 375 |
+
|
| 376 |
+
print('iniciando filtro')
|
| 377 |
+
# 1. Pega o texto, deixa minúsculo e tira espaços sobrando nas pontas
|
| 378 |
+
texto = e.control.value.lower().strip()
|
| 379 |
+
|
| 380 |
+
# 2. Se apagou tudo, devolve a tabela original
|
| 381 |
+
if texto == "" or texto== " ":
|
| 382 |
+
preencher_tabela(df.head(50))
|
| 383 |
+
print('sem dados para filtrar')
|
| 384 |
+
return
|
| 385 |
+
|
| 386 |
+
# 3. O SEGREDO: Corta o texto nos espaços!
|
| 387 |
+
# Se ele digitou "AGRO 3000", vira uma lista: ["agro", "3000"]
|
| 388 |
+
palavras_digitadas = texto.split()
|
| 389 |
+
|
| 390 |
+
# Fazemos uma cópia da tabela inteira para ir "espremendo" ela
|
| 391 |
+
df_filtrado = df.copy()
|
| 392 |
+
|
| 393 |
+
# 4. O Loop do Afunilamento
|
| 394 |
+
for palavra in palavras_digitadas:
|
| 395 |
+
|
| 396 |
+
print(f'Filtrando as palavras:{palavra}')
|
| 397 |
+
# Para CADA palavra, ele procura na tabela e joga fora o que não bater
|
| 398 |
+
mascara_busca = df_filtrado.astype(str).apply(
|
| 399 |
+
lambda col: col.str.lower().str.contains(palavra, na=False))
|
| 400 |
+
df_filtrado = df_filtrado[mascara_busca.any(axis=1)]
|
| 401 |
+
|
| 402 |
+
# No final do loop, só sobram as linhas que têm TODAS as palavras digitadas!
|
| 403 |
+
preencher_tabela(df_filtrado.head(50))
|
| 404 |
+
|
| 405 |
+
# Campo de busca inteligente
|
| 406 |
+
campo_busca = ft.TextField(
|
| 407 |
+
label="Filtrar qualquer campo...",
|
| 408 |
+
prefix_icon=ft.Icons.SEARCH,
|
| 409 |
+
width=250,
|
| 410 |
+
height=40,
|
| 411 |
+
text_size=14,
|
| 412 |
+
border_color="white24",
|
| 413 |
+
focused_border_color="green",
|
| 414 |
+
label_style=ft.TextStyle(color="white70"),
|
| 415 |
+
on_submit=filtrar_tudo # Chama a função quando clicamos no enter
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
#Etapa onde definimos valores fora da tabela principal, como botões de atualizar, abrir novas abas etc
|
| 422 |
+
|
| 423 |
+
# --- O CABEÇALHO RESPONSIVO ---
|
| 424 |
+
# --- O CABEÇALHO RESPONSIVO ---
|
| 425 |
+
linha_cabecalho = ft.Row(
|
| 426 |
+
controls=[
|
| 427 |
+
|
| 428 |
+
# 1. O TEXTO DO APLICATIVO (O jeito certo de colocar texto)
|
| 429 |
+
# Remova o texto da sua imagem PNG e deixe o Flet desenhar ele aqui,
|
| 430 |
+
# assim ele NUNCA mais será cortado em telas menores!
|
| 431 |
+
ft.Text("App Custo de Produtos - Setor Compras", color="white", weight="bold", size=18),
|
| 432 |
+
|
| 433 |
+
# 2. PRIMEIRA MOLA: Fica entre o título e o grupo central
|
| 434 |
+
ft.Container(expand=True),
|
| 435 |
+
|
| 436 |
+
# 3. O "PACOTE" CENTRAL: Agrupamos a busca e os botões juntos aqui
|
| 437 |
+
# 3. O "PACOTE" CENTRAL: Busca, Botões e Linhas Divisórias
|
| 438 |
+
ft.Row(
|
| 439 |
+
controls=[
|
| 440 |
+
# O Campo de busca fica de um lado
|
| 441 |
+
campo_busca,
|
| 442 |
+
|
| 443 |
+
# --- LINHA DIVISÓRIA 1 (Feita pelo Flet) ---
|
| 444 |
+
ft.Container(
|
| 445 |
+
width=1, # Espessura da linha
|
| 446 |
+
height=30, # Altura da linha
|
| 447 |
+
bgcolor="white54", # Branco com um pouco de transparência
|
| 448 |
+
margin=ft.margin.symmetric(horizontal=10) # Dá um respiro para os lados
|
| 449 |
+
),
|
| 450 |
+
|
| 451 |
+
# O primeiro botão fica no meio
|
| 452 |
+
ft.TextButton("Lista De Produtos", style=ft.ButtonStyle(
|
| 453 |
+
color={ft.ControlState.DEFAULT: ft.Colors.WHITE,
|
| 454 |
+
ft.ControlState.HOVERED: ft.Colors.BLACK})),
|
| 455 |
+
|
| 456 |
+
# --- LINHA DIVISÓRIA 2 (Feita pelo Flet) ---
|
| 457 |
+
ft.Container(
|
| 458 |
+
width=1,
|
| 459 |
+
height=30,
|
| 460 |
+
bgcolor="white54",
|
| 461 |
+
margin=ft.margin.symmetric(horizontal=10)
|
| 462 |
+
),
|
| 463 |
+
|
| 464 |
+
# O segundo botão fica do outro lado
|
| 465 |
+
ft.TextButton("Meus Registros", on_click=ir_para_registros, style=ft.ButtonStyle(
|
| 466 |
+
color={ft.ControlState.DEFAULT: ft.Colors.WHITE,
|
| 467 |
+
ft.ControlState.HOVERED: ft.Colors.BLACK})),
|
| 468 |
+
],
|
| 469 |
+
spacing=5, # O spacing pode ser menor agora, pois a margem das linhas já afasta os itens
|
| 470 |
+
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
| 471 |
+
),
|
| 472 |
+
|
| 473 |
+
# 4. SEGUNDA MOLA: Fica entre o grupo central e o botão de sair
|
| 474 |
+
ft.Container(expand=True),
|
| 475 |
+
|
| 476 |
+
# 5. BOTÃO DE SAIR (A segunda mola empurra ele lá pro canto direito)
|
| 477 |
+
ft.Container(
|
| 478 |
+
content=ft.Icon(icon=ft.Icons.POWER_SETTINGS_NEW, color="white", size=20),
|
| 479 |
+
on_click=ir_para_login,
|
| 480 |
+
tooltip="Sair do Sistema!",
|
| 481 |
+
bgcolor="red",
|
| 482 |
+
width=35,
|
| 483 |
+
height=35,
|
| 484 |
+
border_radius=5,
|
| 485 |
+
alignment=ft.Alignment.CENTER,
|
| 486 |
+
ink=True,
|
| 487 |
+
)
|
| 488 |
+
],
|
| 489 |
+
alignment=ft.MainAxisAlignment.START,
|
| 490 |
+
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
# --- A ÁREA DA TABELA RESPONSIVA ---
|
| 494 |
+
area_tabela = ft.Container(
|
| 495 |
+
content=ft.Row(
|
| 496 |
+
controls=[
|
| 497 |
+
ft.Column(
|
| 498 |
+
controls=[
|
| 499 |
+
tabela_cabecalho,
|
| 500 |
+
container_colum
|
| 501 |
+
],
|
| 502 |
+
spacing=0,
|
| 503 |
+
expand=True # Permite que a coluna da tabela cresça verticalmente
|
| 504 |
+
)
|
| 505 |
+
],
|
| 506 |
+
scroll=ft.ScrollMode.AUTO, # Mantém o scroll horizontal essencial para tabelas grandes
|
| 507 |
+
expand=True,
|
| 508 |
+
vertical_alignment=ft.CrossAxisAlignment.START
|
| 509 |
+
),
|
| 510 |
+
bgcolor="white",
|
| 511 |
+
border_radius=10,
|
| 512 |
+
padding=10,
|
| 513 |
+
expand=True # Faz a caixa branca da tabela ocupar todo o espaço restante da tela
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
# --- O LAYOUT PRINCIPAL (Substitui o Stack) ---
|
| 517 |
+
# --- O LAYOUT PRINCIPAL (Atualizado Flet 0.22+) ---
|
| 518 |
+
conteudo = ft.Container(
|
| 519 |
+
# NOVA FORMA DE COLOCAR IMAGEM DE FUNDO:
|
| 520 |
+
image=ft.DecorationImage(
|
| 521 |
+
src="BP - Analítico-App-Custo-v2 (1).png",
|
| 522 |
+
fit="cover"
|
| 523 |
+
),
|
| 524 |
+
expand=True,
|
| 525 |
+
# NOVA FORMA DE FAZER O PADDING (Resolve o aviso amarelo):
|
| 526 |
+
padding=ft.Padding(top=20, left=30, right=30, bottom=20),
|
| 527 |
+
content=ft.Column(
|
| 528 |
+
controls=[
|
| 529 |
+
linha_cabecalho,
|
| 530 |
+
area_tabela # A tabela vai preencher todo o resto do espaço
|
| 531 |
+
],
|
| 532 |
+
expand=True,
|
| 533 |
+
spacing=20 # Espaçamento dinâmico entre o cabeçalho e a tabela
|
| 534 |
+
)
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
# Como tiramos o Stack, o banner agora vai na "camada superior" da página (Overlay)
|
| 538 |
+
# Isso garante que ele sempre flutue no topo da tela, independente do tamanho!
|
| 539 |
+
page.overlay.append(banner_customizado)
|
| 540 |
+
|
| 541 |
+
page.add(conteudo)
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
preencher_tabela(df.head(50))
|
| 545 |
+
|
| 546 |
+
# Assim que abrir a tela de login chamamos a def que irá buscar os dados do QLik
|
| 547 |
+
page.run_task(carregar_no_fundo)
|
| 548 |
+
ir_para_login()
|
| 549 |
+
|
| 550 |
+
#ft.run(main, view=ft.AppView.WEB_BROWSER, assets_dir=".") # Removido o modo browser para rodar como App Desktop nativo
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
ft.run(
|
| 554 |
+
main,
|
| 555 |
+
view=ft.WEB_BROWSER,
|
| 556 |
+
assets_dir=".", # <--- Mantém as suas imagens funcionando!
|
| 557 |
+
host="0.0.0.0", # <--- Libera o acesso externo
|
| 558 |
+
port=7860 # <--- A porta obrigatória do Hugging Face
|
| 559 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flet
|
| 2 |
+
pandas
|
| 3 |
+
python-dotenv
|
| 4 |
+
google-auth
|
| 5 |
+
gspread
|
| 6 |
+
qlik-sdk
|
| 7 |
+
requests
|