Sistema completo para gerenciamento de cotações e compras diretas, integrado ao ERP TOTVS-Protheus. Com autenticação de usuários, dashboard de indicadores, geração automática de e-mails para aprovação e Power BI integration.
Criação de solicitações com até 3 cotações de fornecedores, cálculo automático de economia e decisão de compra.
Registro de compras sem processo de cotação, com importação de pedidos do Protheus por número.
Consulta em tempo real de produtos, centros de custo e pedidos de compra diretamente do ERP TOTVS-Protheus via pyodbc.
KPIs em tempo real: gasto total, economia gerada, ranking de fornecedores e gastos por centro de custo.
Geração automática de e-mail formatado para aprovação da diretoria com todas as cotações abertas.
Views SQL otimizadas para consumo direto no Power BI Desktop com guia passo a passo de conexão.
Sistema de login com tokens SHA-256, níveis de acesso (admin/user) e gerenciamento de usuários.
Interface dark theme adaptável, com abas de navegação, badges e notificações toast.
| Produto | CC | Qtd | Forn 1 | Forn 2 | Forn 3 | Status |
|---|---|---|---|---|---|---|
| EQ00123 - Parafuso | PRODUÇÃO | 500 | R$ 2,50 | R$ 2,30 | R$ 2,80 | ✓ Fechada |
| EQ00456 - Luva | SEGURANÇA | 100 | R$ 8,90 | R$ 9,20 | R$ 8,50 | ○ Aberta |
| MP00999 - Resina | MATÉRIA PRIMA | 1000 | Compra Direta | ✓ Fechada | ||
"""
Sistema de Cotação — Backend Flask + SQLite
Integracao com TOTVS Protheus via pyodbc
"""
from flask import Flask, request, jsonify, send_from_directory
import sqlite3, os, hashlib, secrets, unicodedata
from datetime import datetime
# ── Protheus SQL Server ──
PROTHEUS_SERVER = "SERVIDOR_PROTHEUS"
PROTHEUS_DATABASE = "BASE_PROTHEUS"
PROTHEUS_USER = "USUARIO_CONSULTA"
PROTHEUS_PASSWORD = "***"
def get_protheus():
import pyodbc
conn_str = (
f"DRIVER={{{PROTHEUS_DRIVER}}};"
f"SERVER={PROTHEUS_SERVER};"
f"DATABASE={PROTHEUS_DATABASE};"
f"UID={PROTHEUS_USER};PWD={PROTHEUS_PASSWORD};"
"TrustServerCertificate=yes;Connection Timeout=10;"
)
return pyodbc.connect(conn_str)
app = Flask(__name__, static_folder="static")
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cotacoes.db")
# ── CORS ──
@app.after_request
def add_cors(response):
response.headers["Access-Control-Allow-Origin"] = "*"
return response
# ── Autenticação ──
def hash_senha(senha):
return hashlib.sha256(senha.encode()).hexdigest()
def gerar_token():
return secrets.token_hex(32)
def requer_login(f):
from functools import wraps
@wraps(f)
def decorated(*args, **kwargs):
token = get_token_request()
user = verificar_token(token)
if not user:
return jsonify({"ok": False, "erro": "Não autorizado"}), 401
return f(*args, user=user, **kwargs)
return decorated
# ── Endpoints da API ──
@app.route("/api/auth/login", methods=["POST"])
def login():
data = request.get_json()
usuario = data.get("usuario", "")
senha = data.get("senha", "")
conn = get_db()
row = conn.execute(
"SELECT id, nome, usuario, role FROM usuarios WHERE usuario = ? AND senha_hash = ? AND ativo = 1",
(usuario, hash_senha(senha))
).fetchone()
if not row:
return jsonify({"ok": False, "erro": "Usuário ou senha inválidos"}), 401
token = gerar_token()
conn.execute("INSERT INTO sessoes (usuario_id, token, criado_em) VALUES (?, ?, ?)",
(row[0], token, now()))
conn.commit()
return jsonify({"ok": True, "token": token, "usuario": row[1], "role": row[3]})
@app.route("/api/dashboard")
@requer_login
def dashboard(user):
return jsonify({
"total_solicitacoes": row[0],
"total_compras_diretas": row2[0],
"gasto_total": gasto,
"economia_gerada": eco,
"total_fornecedores": forn,
"gastos_por_cc": gastos_cc,
"top_fornecedores": top_forn
})
# ── Endpoints Protheus ──
@app.route("/api/protheus/produto/<codigo>")
@requer_login
def buscar_produto_protheus(codigo, user):
conn = get_protheus()
cursor = conn.cursor()
cursor.execute("""
SELECT B1_COD, B1_DESC, B1_UM
FROM SB1010
WHERE B1_COD = ? AND D_E_L_E_T_ = ''
""", (codigo,))
row = cursor.fetchone()
if not row:
return jsonify({"ok": False, "erro": "Produto nao encontrado"}), 404
return jsonify({"ok": True, "codigo": row[0], "descricao": row[1], "unidade": row[2]})
# ── Inicialização ──
if __name__ == "__main__":
init_db()
app.run(host="192.168.15.7", port=50001, debug=False)
-- Sistema de Cotação — SQLite Schema
-- 6 tabelas + 4 views Power BI
-- Usuários do sistema
CREATE TABLE usuarios (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
usuario TEXT NOT NULL UNIQUE,
senha_hash TEXT NOT NULL,
role TEXT DEFAULT 'user',
ativo INTEGER DEFAULT 1,
criado_em TEXT NOT NULL
);
-- Sessões de autenticação (token-based)
CREATE TABLE sessoes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
usuario_id INTEGER NOT NULL,
token TEXT NOT NULL UNIQUE,
criado_em TEXT NOT NULL,
FOREIGN KEY (usuario_id) REFERENCES usuarios(id) ON DELETE CASCADE
);
-- Solicitações de cotação
CREATE TABLE solicitacoes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sc_numero TEXT,
produto TEXT NOT NULL,
descricao TEXT,
cc_nome TEXT,
cc_numero TEXT,
quantidade REAL NOT NULL,
unidade TEXT DEFAULT 'UN',
data_necessidade TEXT,
status TEXT DEFAULT 'aberta',
obs_geral TEXT,
criado_em TEXT NOT NULL,
atualizado_em TEXT
);
-- Cotações de fornecedores (até 3 por solicitação)
CREATE TABLE cotacoes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
solicitacao_id INTEGER NOT NULL,
numero_fornecedor INTEGER NOT NULL,
nome_fornecedor TEXT,
preco_unitario REAL DEFAULT 0,
preco_total REAL DEFAULT 0,
frete REAL DEFAULT 0,
prazo_entrega TEXT,
condicao_pagamento TEXT,
FOREIGN KEY (solicitacao_id) REFERENCES solicitacoes(id) ON DELETE CASCADE
);
-- Decisões de compra (uma por solicitação)
CREATE TABLE decisoes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
solicitacao_id INTEGER NOT NULL UNIQUE,
fornecedor_escolhido TEXT,
numero_fornecedor INTEGER,
preco_unitario REAL,
preco_total REAL,
economia_gerada REAL,
justificativa TEXT,
aprovado_por TEXT,
aprovado_em TEXT,
FOREIGN KEY (solicitacao_id) REFERENCES solicitacoes(id) ON DELETE CASCADE
);
-- Compras diretas (sem processo de cotação)
CREATE TABLE compras_diretas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sc_numero TEXT,
produto TEXT NOT NULL,
descricao TEXT,
cc_nome TEXT,
cc_numero TEXT,
quantidade REAL NOT NULL,
unidade TEXT DEFAULT 'UN',
preco_unitario REAL DEFAULT 0,
preco_total REAL DEFAULT 0,
fornecedor TEXT,
condicao_pagamento TEXT,
data_entrega TEXT,
status TEXT DEFAULT 'aberta',
obs TEXT,
criado_em TEXT NOT NULL,
atualizado_em TEXT
);
<!-- Frontend: Sistema de Cotação — Single Page Application -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<title>SC · Sistema de Cotação</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Mono&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- Todo o CSS inline no <style> (dark theme, ~170 linhas) -->
</head>
<body>
<!-- ══ TELA DE LOGIN ══ -->
<div id="login-screen">
<div class="login-box">
<div class="login-badge">SC</div>
<h3>Sistema de Cotação</h3>
<p class="login-sub">Suprimentos & Compras · Trimplas</p>
<input id="login-usuario" type="text" placeholder="usuario">
<input id="login-senha" type="password" placeholder="senha">
<button onclick="fazerLogin()">Entrar</button>
</div>
</div>
<!-- ══ APP (visível após login) ══ -->
<div id="app-screen">
<!-- Header -->
<header>
<span class="logo-badge">SC</span>
<span>Sistema de Cotação</span>
<div class="db-status">
<span class="db-dot"></span>
<span id="db-status-txt">Online</span>
</div>
<button onclick="fazerLogout()">Sair</button>
</header>
<!-- Abas de navegação -->
<div class="tabs">
<div onclick="goTab('nova')">+ Nova Cotação</div>
<div onclick="goTab('lista')">☰ Cotações</div>
<div onclick="goTab('direta')">🛒 Compra Direta</div>
<div onclick="goTab('dash')">📊 Dashboard</div>
<div onclick="goTab('email')">✉ E-mail</div>
<div onclick="goTab('powerbi')">⚡ Power BI</div>
</div>
<!-- Página: Nova Cotação -->
<div class="page active" id="page-nova">
<div class="form-grid">
<input id="sc-numero" placeholder="Nº da SC">
<input id="sc-produto" placeholder="Código do produto">
<button onclick="buscarProduto()">🔍 Buscar no Protheus</button>
<input id="sc-descricao" placeholder="Descrição">
<input id="sc-cc-nome" placeholder="Centro de Custo">
<input type="number" id="sc-qtd" placeholder="Quantidade">
</div>
<!-- 3 fornecedores -->
<div class="forn-grid">
<div class="forn-card">
<h4>① Fornecedor 1</h4>
<input id="f1-nome" placeholder="Nome">
<input type="number" id="f1-preco" placeholder="Preço unitário">
</div>
<div class="forn-card">
<h4>② Fornecedor 2</h4>
<input id="f2-nome" placeholder="Nome">
<input type="number" id="f2-preco" placeholder="Preço unitário">
</div>
<div class="forn-card">
<h4>③ Fornecedor 3</h4>
<input id="f3-nome" placeholder="Nome">
<input type="number" id="f3-preco" placeholder="Preço unitário">
</div>
</div>
<!-- Decisão -->
<select id="dec-forn">
<option value="">Fornecedor escolhido...</option>
</select>
<button onclick="salvar()">Salvar Cotação</button>
</div>
</div>
<script>
// app.js: lógica completa do frontend (~600 linhas)
// Inclui: login, CRUD de cotações, compras diretas,
// dashboard, geração de e-mail, integração Protheus
</script>
</body>
</html>
-- Views SQL otimizadas para Power BI Desktop
-- Cotacoes para Power BI (denormalizada com 3 fornecedores)
CREATE VIEW vw_cotacoes_powerbi AS
SELECT
s.id AS solicitacao_id,
s.sc_numero,
s.produto,
s.descricao,
s.cc_nome AS centro_de_custo,
s.cc_numero,
s.quantidade,
s.unidade,
s.data_necessidade,
s.status,
s.criado_em,
MAX(CASE WHEN c.numero_fornecedor = 1 THEN c.nome_fornecedor END) AS forn1_nome,
MAX(CASE WHEN c.numero_fornecedor = 1 THEN c.preco_unitario END) AS forn1_preco_unit,
MAX(CASE WHEN c.numero_fornecedor = 1 THEN c.preco_total END) AS forn1_preco_total,
MAX(CASE WHEN c.numero_fornecedor = 2 THEN c.nome_fornecedor END) AS forn2_nome,
MAX(CASE WHEN c.numero_fornecedor = 2 THEN c.preco_unitario END) AS forn2_preco_unit,
MAX(CASE WHEN c.numero_fornecedor = 2 THEN c.preco_total END) AS forn2_preco_total,
MAX(CASE WHEN c.numero_fornecedor = 3 THEN c.nome_fornecedor END) AS forn3_nome,
MAX(CASE WHEN c.numero_fornecedor = 3 THEN c.preco_unitario END) AS forn3_preco_unit,
MAX(CASE WHEN c.numero_fornecedor = 3 THEN c.preco_total END) AS forn3_preco_total,
d.fornecedor_escolhido,
d.preco_unitario AS decisao_preco_unit,
d.preco_total AS decisao_preco_total,
d.economia_gerada,
d.justificativa,
d.aprovado_por,
d.aprovado_em
FROM solicitacoes s
LEFT JOIN cotacoes c ON c.solicitacao_id = s.id
LEFT JOIN decisoes d ON d.solicitacao_id = s.id
GROUP BY s.id;
-- Compras Diretas para Power BI
CREATE VIEW vw_compras_diretas_powerbi AS
SELECT
id, sc_numero, produto, descricao,
cc_nome AS centro_de_custo, cc_numero,
quantidade, unidade, preco_unitario, preco_total,
fornecedor, condicao_pagamento, data_entrega,
status, obs, criado_em
FROM compras_diretas;
-- Ranking de fornecedores por vitórias
CREATE VIEW vw_fornecedores_ranking AS
SELECT
d.fornecedor_escolhido AS fornecedor,
COUNT(*) AS vitorias,
SUM(d.preco_total) AS volume_total,
SUM(d.economia_gerada) AS economia_gerada
FROM decisoes d
GROUP BY d.fornecedor_escolhido
ORDER BY vitorias DESC;
-- Gastos por centro de custo (mensal)
CREATE VIEW vw_gastos_por_cc AS
SELECT
cc_nome AS centro_de_custo,
strftime('%Y-%m', criado_em) AS mes,
SUM(preco_total) AS gasto_total,
COUNT(*) AS total_compras
FROM (
SELECT cc_nome, preco_total, criado_em FROM decisoes d
JOIN solicitacoes s ON s.id = d.solicitacao_id
UNION ALL
SELECT cc_nome, preco_total, criado_em FROM compras_diretas
)
GROUP BY cc_nome, mes
ORDER BY mes DESC, gasto_total DESC;