Sistema de Cotações / Compras Diretas

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.

PythonFlaskSQLiteSQL ServerJavaScriptPower BI

📋 Gestão de Cotações

Criação de solicitações com até 3 cotações de fornecedores, cálculo automático de economia e decisão de compra.

🛒 Compras Diretas

Registro de compras sem processo de cotação, com importação de pedidos do Protheus por número.

🔗 Integração Protheus

Consulta em tempo real de produtos, centros de custo e pedidos de compra diretamente do ERP TOTVS-Protheus via pyodbc.

📊 Dashboard

KPIs em tempo real: gasto total, economia gerada, ranking de fornecedores e gastos por centro de custo.

✉ E-mail para Aprovação

Geração automática de e-mail formatado para aprovação da diretoria com todas as cotações abertas.

⚡ Power BI

Views SQL otimizadas para consumo direto no Power BI Desktop com guia passo a passo de conexão.

👥 Autenticação

Sistema de login com tokens SHA-256, níveis de acesso (admin/user) e gerenciamento de usuários.

📱 Design Responsivo

Interface dark theme adaptável, com abas de navegação, badges e notificações toast.

Pré-visualização

Sistema de Cotação
Cotações
47
Compras Diretas
23
Total Gasto
R$ 128K
Economia
R$ 18,5K
ProdutoCCQtdForn 1Forn 2Forn 3Status
EQ00123 - ParafusoPRODUÇÃO500R$ 2,50R$ 2,30R$ 2,80✓ Fechada
EQ00456 - LuvaSEGURANÇA100R$ 8,90R$ 9,20R$ 8,50○ Aberta
MP00999 - ResinaMATÉRIA PRIMA1000Compra Direta✓ Fechada

Código do Projeto

app.py
Database
frontend/index.html
Power BI Views
"""
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;