Um sistema completo de gestão de estoque desenvolvido em Python com CustomTkinter. Combina interface moderna dark mode com banco de dados SQLite robusto para controle de produtos, ajustes em tempo real, histórico de movimentações, quadro de avisos da equipe, dashboard inteligente e exportação para CSV.
SVP MARKET é um sistema enterprise-ready de gestão de estoque que implementa CRUD completo para produtos, controle de movimentações, histórico auditável, comunicação da equipe e relatórios visuais. Construído com arquitetura desacoplada (separação Model/View/Database), é altamente mantível e extensível.
- 📦 CRUD Completo: Criar, ler, atualizar e deletar produtos
- 📊 Dashboard Inteligente: Métricas em tempo real com 4 cards principais
- 🔍 Busca Avançada: Filtro por nome, ID, categoria em tempo real
- 📈 Histórico Detalhado: Rastreamento de todas as movimentações
- 💰 Cálculo de Estoque: Valor total em tempo real
⚠️ Alertas: Produtos com estoque baixo ou zerado- 📝 Quadro de Avisos: Sistema de notas colaborativas
- 🔄 Ajuste Rápido: Interface dedicada para ±N unidades
- 📊 Exportar CSV: Backup e análise em Excel/Sheets
- 🌓 Tema Light/Dark: Comutável em tempo real
- 🏷️ Categorias: Organização por tipos de produtos
- ⌨️ Atalhos: Ctrl+N (novo), Ctrl+F (buscar)
📊 Dashboard
├─ 4 Cards de Métricas:
│ ├─ 📦 Total de Itens: 4 produtos
│ ├─ 💰 Valor em Estoque: R$ 250,50
│ ├─ ⚠️ Em Alerta: 2 produtos (estoque < 10)
│ └─ ❌ Sem Estoque: 1 produto (quantidade = 0)
│
└─ Itens por Categoria:
├─ Alimentos: 2 produtos, 57 unidades
├─ Laticínios: 1 produto, 50 unidades
└─ Limpeza: 1 produto, 12 unidades
Operações no Inventário:
1. BUSCAR/FILTRAR:
├─ Campo de busca por nome/ID/categoria
├─ Dropdown de categorias
└─ Resultado em tempo real
2. VISUALIZAR:
├─ Cada produto em card com:
│ ├─ Nome do produto
│ ├─ ID único
│ ├─ Categoria e preço
│ ├─ Nota interna (se houver)
│ ├─ Quantidade grande e colorida
│ └─ Indicador visual de status
│
└─ 4 Botões de ação:
├─ ± Ajustar estoque
├─ 🕐 Ver histórico
├─ ✏️ Editar
└─ 🗑 Deletar
3. ADICIONAR PRODUTO:
├─ Ctrl+N ou clique em ➕
├─ Preencher formulário:
│ ├─ ID (obrigatório, único)
│ ├─ Nome (obrigatório)
│ ├─ Preço (numérico, >= 0)
│ ├─ Quantidade (inteiro, >= 0)
│ ├─ Categoria
│ └─ Nota interna
│
└─ SALVAR ou Cancelar
┌─────────────────────────────────────┐
│ [🟢 BARRA] Nome do Produto │
│ #ABC123 • Categoria • R$ 10,50 │
│ 📌 Nota interna (se houver) │
│ ESTOQUE BAIXO (se < 10) │
│ 50 unid │
│ [±] [🕐] [✏️] [🗑] │
└─────────────────────────────────────┘
Cores de Status:
🟢 Verde: Estoque ok (qtd >= 10)
🟠 Amarelo: Alerta (0 < qtd < 10)
🔴 Vermelho: Zerado (qtd = 0)
Funcionalidade: Comunicação interna da equipe
1. POSTAR AVISO:
├─ Campo de mensagem
├─ Campo do autor
└─ Botão POSTAR (Enter também funciona)
2. VISUALIZAR:
├─ Ordem inversa (últimas primeiro)
├─ Cada nota mostra:
│ ├─ Mensagem
│ ├─ Autor
│ ├─ Data/hora de criação
│ └─ Botão ✕ para remover
│
└─ Exemplo:
"⚠️ Freezer 3 com barulho estranho"
— João (Manhã) • 2026-05-08 14:30
[✕]
3. REMOVER:
└─ Clique no ✕ e confirme
CREATE TABLE produtos (
id TEXT PRIMARY KEY,
nome TEXT NOT NULL,
preco REAL NOT NULL CHECK(preco >= 0),
qtd INTEGER NOT NULL DEFAULT 0 CHECK(qtd >= 0),
categoria TEXT NOT NULL DEFAULT 'Geral',
nota TEXT DEFAULT '',
criado_em TEXT DEFAULT (datetime('now')),
atualizado_em TEXT DEFAULT (datetime('now'))
)Exemplo de Dados:
id | nome | preco | qtd | categoria | nota
------|-------------------|-------|-----|------------|----------------------------------
001 | Arroz 5kg | 25.90 | 4 | Alimentos | Conferir validade lote B
002 | Leite Integral | 5.40 | 50 | Laticínios | (vazio)
003 | Detergente | 2.20 | 12 | Limpeza | Reposição chega terça
004 | Café 500g | 18.00 | 3 | Alimentos | Cliente reclamou do preço
CREATE TABLE historico_estoque (
id INTEGER PRIMARY KEY AUTOINCREMENT,
produto_id TEXT NOT NULL,
tipo TEXT NOT NULL CHECK(tipo IN ('entrada','saida','ajuste')),
quantidade INTEGER NOT NULL,
obs TEXT DEFAULT '',
criado_em TEXT DEFAULT (datetime('now')),
FOREIGN KEY (produto_id) REFERENCES produtos(id) ON DELETE CASCADE
)Rastreamento Automático:
- "entrada": Produto adicionado ao estoque
- "saida": Produto removido do estoque
- "ajuste": Correção manual de quantidade
Exemplo:
id | produto_id | tipo | quantidade | obs | criado_em
---|------------|---------|------------|------------------|-------------------
1 | 001 | entrada | 4 | Cadastro inicial | 2026-05-08 10:00
2 | 002 | entrada | 50 | Cadastro inicial | 2026-05-08 10:00
3 | 002 | saida | 5 | Venda balcão | 2026-05-08 14:30
CREATE TABLE notas_equipe (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mensagem TEXT NOT NULL,
autor TEXT NOT NULL DEFAULT 'Anônimo',
criado_em TEXT DEFAULT (datetime('now'))
)┌─────────────────────────────────────────┐
│ main.py (View) │
│ Interface CustomTkinter │
│ ┌───────────────────────────────────┐ │
│ │ - SupermarketPro (CTkTk) │ │
│ │ - JanelaProduto (CTkToplevel) │ │
│ │ - JanelaAjuste (CTkToplevel) │ │
│ │ - JanelaHistorico (CTkToplevel) │ │
│ └──────────┬──────────────────────┘ │
└─────────────┼──────────────────────────┘
│
│ Chamadas de função
│
┌─────────────▼──────────────────────────┐
│ database.py (Model) │
│ Lógica e Persistência │
│ ┌───────────────────────────────────┐ │
│ │ Funções: │ │
│ │ - listar_produtos() │ │
│ │ - inserir_produto() │ │
│ │ - atualizar_produto() │ │
│ │ - deletar_produto() │ │
│ │ - ajustar_estoque() │ │
│ │ - listar_notas() │ │
│ │ - resumo_dashboard() │ │
│ └──────────┬──────────────────────┘ │
└─────────────┼──────────────────────────┘
│
│ SQL Queries
│
┌─────────────▼──────────────────────────┐
│ estoque.db (SQLite) │
│ Dados Persistentes │
│ ┌───────────────────────────────────┐ │
│ │ - produtos │ │
│ │ - historico_estoque │ │
│ │ - notas_equipe │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
def get_connection() -> sqlite3.Connection:
"""Retorna conexão SQLite com configurações."""
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row # Permite dict(row)
conn.execute("PRAGMA foreign_keys = ON") # Habilita chaves estrangeiras
return conn
# Vantagens:
# - Cria diretório se não existir
# - row_factory: converte Row → dict
# - Foreign keys: integridade referencialdef init_db() -> None:
"""Cria tabelas se não existirem. Seguro rodar múltiplas vezes."""
with get_connection() as conn:
conn.executescript("""
CREATE TABLE IF NOT EXISTS produtos ( ... );
CREATE TABLE IF NOT EXISTS notas_equipe ( ... );
CREATE TABLE IF NOT EXISTS historico_estoque ( ... );
""")
_seed(conn) # Carrega dados iniciaisPadrão Idempotente: Usar "CREATE TABLE IF NOT EXISTS"
- ✅ Primeira execução: cria tabelas
- ✅ Próximas execuções: não tenta criar novamente
- ✅ Seguro rodar no init da app
def listar_produtos(filtro: str = "", categoria: str = "") -> list[dict]:
"""Retorna produtos com filtros opcionais."""
with get_connection() as conn:
query = "SELECT * FROM produtos WHERE 1=1"
params: list = []
# FILTRO POR NOME/ID/CATEGORIA
if filtro.strip():
query += " AND (nome LIKE ? OR categoria LIKE ? OR id LIKE ?)"
params += [f"%{filtro}%"] * 3
# FILTRO POR CATEGORIA
if categoria.strip() and categoria != "Todas":
query += " AND categoria = ?"
params.append(categoria)
query += " ORDER BY nome COLLATE NOCASE"
return [dict(r) for r in conn.execute(query, params).fetchall()]
# Busca Case-Insensitive com LIKE:
# "Arr" encontra "Arroz", "ARROZ", "arroz"
# Exemplo:
listar_produtos("arr", "Alimentos")
# → Retorna: [{"id": "001", "nome": "Arroz 5kg", ...}]def inserir_produto(id: str, nome: str, preco: float, qtd: int,
categoria: str, nota: str = "") -> None:
"""Insere novo produto e registra no histórico."""
with get_connection() as conn:
conn.execute(
"INSERT INTO produtos (id, nome, preco, qtd, categoria, nota) "
"VALUES (?,?,?,?,?,?)",
(id, nome, preco, qtd, categoria, nota),
)
# Automaticamente registra no histórico como "entrada"
_reg_hist(conn, id, "entrada", qtd, "Cadastro inicial")
# Garantias:
# - Levanta erro se ID duplicado (PRIMARY KEY)
# - Levanta erro se preco < 0 (CHECK constraint)
# - Levanta erro se qtd < 0 (CHECK constraint)def ajustar_estoque(produto_id: str, quantidade: int, obs: str = "") -> None:
"""Ajusta quantidade (positivo=entrada, negativo=saida)."""
with get_connection() as conn:
# 1. OBTER ESTOQUE ATUAL
p = conn.execute(
"SELECT qtd FROM produtos WHERE id=?",
(produto_id,)
).fetchone()
if not p:
raise ValueError(f"Produto {produto_id} não encontrado.")
# 2. CALCULAR NOVA QUANTIDADE
nova_qtd = p["qtd"] + quantidade
# 3. VALIDAR (não deixa negativo)
if nova_qtd < 0:
raise ValueError("Estoque não pode ficar negativo.")
# 4. ATUALIZAR
conn.execute(
"UPDATE produtos SET qtd=?, atualizado_em=datetime('now') WHERE id=?",
(nova_qtd, produto_id),
)
# 5. REGISTRAR NO HISTÓRICO
tipo = "entrada" if quantidade > 0 else "saida"
_reg_hist(conn, produto_id, tipo, abs(quantidade),
obs or "Ajuste rápido")
# Exemplos:
ajustar_estoque("002", 10, "Recebimento") # +10 unidades
ajustar_estoque("002", -5, "Venda") # -5 unidades
ajustar_estoque("002", -999) # ERROR: estoque negativodef resumo_dashboard() -> dict:
"""Retorna métricas para dashboard."""
with get_connection() as conn:
total_itens = conn.execute(
"SELECT COUNT(*) FROM produtos"
).fetchone()[0]
valor_total = conn.execute(
"SELECT COALESCE(SUM(preco * qtd), 0) FROM produtos"
).fetchone()[0]
em_alerta = conn.execute(
"SELECT COUNT(*) FROM produtos WHERE qtd < 10"
).fetchone()[0]
sem_estoque = conn.execute(
"SELECT COUNT(*) FROM produtos WHERE qtd = 0"
).fetchone()[0]
por_cat = conn.execute(
"SELECT categoria, COUNT(*) as total, SUM(qtd) as qtd_total "
"FROM produtos GROUP BY categoria ORDER BY total DESC"
).fetchall()
return {
"total_itens": total_itens,
"valor_total": round(valor_total, 2),
"em_alerta": em_alerta,
"sem_estoque": sem_estoque,
"por_categoria": [dict(r) for r in por_cat],
}
# Retorno:
{
"total_itens": 4,
"valor_total": 250.50,
"em_alerta": 2,
"sem_estoque": 1,
"por_categoria": [
{"categoria": "Alimentos", "total": 2, "qtd_total": 57},
{"categoria": "Laticínios", "total": 1, "qtd_total": 50},
{"categoria": "Limpeza", "total": 1, "qtd_total": 12}
]
}class JanelaProduto(ctk.CTkToplevel):
"""Janela modal para cadastro/edição de produtos."""
CAMPOS = [
("ID do Produto *", "id", False),
("Nome *", "nome", False),
("Preço (R$) *", "preco", False),
("Quantidade *", "qtd", False),
("Categoria", "categoria", False),
("Nota interna", "nota", False),
]
def __init__(self, master, produto: dict | None = None, on_salvar=None):
"""
master: janela pai
produto: dict do produto (None = novo)
on_salvar: callback(dados, editando=bool)
"""
super().__init__(master)
self.title("Novo Produto" if produto is None else "Editar Produto")
self.geometry("500x430")
self.grab_set() # Modal
self.lift() # Traz à frente
# Layout de campos
for i, (label, chave, _) in enumerate(self.CAMPOS):
ctk.CTkLabel(self, text=label).grid(row=i, column=0, sticky="w")
ent = ctk.CTkEntry(self, width=300, height=36)
ent.grid(row=i, column=1)
self.entradas[chave] = ent
# Se editando, preencher com dados antigos
if produto:
self.entradas["id"].insert(0, produto.get("id", ""))
self.entradas["id"].configure(state="disabled") # ID não editável
for k in ("nome", "preco", "qtd", "categoria", "nota"):
self.entradas[k].insert(0, str(produto.get(k, "")))
# Atalhos
self.bind("<Return>", lambda _: self._salvar()) # Enter = salvar
self.bind("<Escape>", lambda _: self.destroy()) # Esc = cancelar
def _salvar(self):
"""Valida e salva produto."""
dados = {k: v.get().strip() for k, v in self.entradas.items()}
# Validações
if not dados["id"] or not dados["nome"]:
messagebox.showerror("Erro", "ID e Nome são obrigatórios.")
return
if not re.match(r'^[A-Za-z0-9_\-]+$', dados["id"]):
messagebox.showerror("Erro", "ID inválido (use A-Z, 0-9, _, -)")
return
try:
dados["preco"] = float(dados["preco"].replace(",", "."))
dados["qtd"] = int(dados["qtd"])
except ValueError:
messagebox.showerror("Erro", "Preço e Qtd devem ser numéricos")
return
if dados["preco"] < 0 or dados["qtd"] < 0:
messagebox.showerror("Erro", "Valores negativos não permitidos")
return
# Callback
if self.on_salvar:
self.on_salvar(dados, editando=self.produto is not None)
self.destroy()def _card_produto(self, parent, p: dict):
"""Cria card visual para um produto."""
# 1. CALCULAR STATUS
em_alerta = 0 < p["qtd"] < LIMITE_ALERTA
sem_estoque = p["qtd"] == 0
status_color = COR_PERIGO if sem_estoque else \
(COR_ALERTA if em_alerta else COR_PRIMARIA)
# 2. CRIAR CARD
card = ctk.CTkFrame(parent, corner_radius=10, height=110)
card.pack(fill="x", pady=5)
card.pack_propagate(False)
# 3. BARRA LATERAL (indicador de status)
indicador = ctk.CTkFrame(card, width=5, fg_color=status_color)
indicador.place(x=0, y=0, relheight=1)
# 4. NOME
ctk.CTkLabel(card, text=p["nome"], font=FONTE_TITULO).place(x=20, y=10)
# 5. INFO (ID • Categoria • Preço)
ctk.CTkLabel(
card,
text=f"#{p['id']} • {p['categoria']} • R$ {p['preco']:.2f}",
font=FONTE_PEQUENA, text_color="#777"
).place(x=20, y=38)
# 6. NOTA (se houver)
if p.get("nota"):
ctk.CTkLabel(
card,
text=f"📌 {p['nota']}",
font=FONTE_PEQUENA,
text_color="#5dade2"
).place(x=20, y=62)
# 7. STATUS TEXT
status_txt = "SEM ESTOQUE" if sem_estoque else \
("ESTOQUE BAIXO" if em_alerta else "")
if status_txt:
ctk.CTkLabel(
card, text=status_txt,
font=("Arial", 10, "bold"),
text_color=status_color
).place(x=20, y=85)
# 8. QUANTIDADE (grande e colorida)
ctk.CTkLabel(
card, text=str(p["qtd"]),
font=("Arial", 26, "bold"),
text_color=status_color
).place(relx=0.72, y=16)
ctk.CTkLabel(
card, text="unid.",
font=FONTE_PEQUENA,
text_color="#666"
).place(relx=0.72, y=50)
# 9. BOTÕES DE AÇÃO (lado direito)
bx = 0.80
for icon, cor_hover, cmd in [
("±", COR_INFO, lambda pid=p["id"]: self._ajustar_estoque(pid)),
("🕐", "#444", lambda pid=p["id"]: self._ver_historico(pid)),
("✏️", "#3a3a3a", lambda pid=p["id"]: self._editar_produto(pid)),
("🗑", COR_PERIGO, lambda pid=p["id"], pn=p["nome"]: self._deletar_produto(pid, pn)),
]:
ctk.CTkButton(
card, text=icon, width=34, height=34,
fg_color="#2c2c2c", hover_color=cor_hover,
command=cmd
).place(relx=bx, y=20)
bx += 0.055
# Visual Result