Skip to content

luisguigui/Market

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

🏪 SVP MARKET — Sistema de Gestão de Estoque

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.

Python CustomTkinter SQLite CSV Export License Status Enterprise


🌟 Visão Geral

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.

✨ Destaques Principais

  • 📦 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)

🎮 Como Usar

🏠 Dashboard

📊 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

📦 Inventário (Gestão de Produtos)

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

⚠️ Status Visual dos Cards

┌─────────────────────────────────────┐
│ [🟢 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)

📝 Quadro de Avisos

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

💾 Modelo de Dados

Tabela: produtos

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

Tabela: historico_estoque

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

Tabela: notas_equipe

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'))
)

🏗️ Arquitetura Completa

📊 Separação Model-View-Database

┌─────────────────────────────────────────┐
│            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                   │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

📚 Documentação de Código

database.py — Camada de Dados

1. Conexão com BD

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 referencial

2. Inicialização (Idempotente)

def 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 iniciais

Padrã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

3. CRUD de Produtos

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", ...}]

4. Inserir Produto

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)

5. Ajustar Estoque

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 negativo

6. Dashboard

def 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}
    ]
}

main.py — Interface Gráfica

1. Classe: JanelaProduto

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()

2. Método: _card_produto()

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

About

Sistema de gestão de estoque em Python com CustomTkinter e SQLite. Interface dark mode com dashboard em tempo real, CRUD completo de produtos, histórico de movimentações, quadro de avisos e exportação CSV.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages