📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Módulo 02: Lógica de Programação e Algoritmos. Recomendado revisar antes de começar.

🌱 Miniprojeto 03: Controle de Estoque da Cantina Escolar

Guia Autoguiado Passo a Passo: Da Manipulação de Listas à Aplicação Desktop com SQLite e Alertas Visuais

Trilha de Especialização — Nível Introdutório (Curso 25: Sistemas Desktop com Python)


🎯 Objetivo de Aprendizagem

Em cantinas escolares, lanchonetes e pequenos quiosques, o controle de estoque de salgados, bebidas e doces é vital para garantir que nenhum produto falte no horário de pico do intervalo.

Neste projeto, o estudante aprende como:

  1. Validar e processar operações com dados numéricos (preços float e quantidades int) em arquivos .txt.
  2. Criar a classe Produto encapsulando as regras de negócio de baixa de estoque na venda, reposição e identificação de estoque crítico.
  3. Persistir os itens na tabela produtos do SQLite3 com consultas filtradas (WHERE quantidade < 5).
  4. Construir uma interface gráfica desktop moderna com painel de métricas, tabela com tags de cores (verde para normal, vermelho para crítico) e botões de ação rápida de venda e reposição.

🪜 O Caminho Didático dos 4 Degraus

flowchart LR
    D1["🪜 DEGRAU 1\nImperativo & .txt\nMenu while + estoque.txt\ncom validação de saldo"] --> D2["🪜 DEGRAU 2\nPrimeira Classe POO\nclass Produto\n(vender, repor, crítico)"]
    D2 --> D3["🪜 DEGRAU 3\nSQLite Nativo\nUPDATE atômico &\nfiltros de quantidade"]
    D3 --> D4["🪜 DEGRAU 4\nInterface Desktop\nCards de métricas, tabela\ne alertas em vermelho"]

    style D1 fill:#fffde7,stroke:#fbc02d,stroke-width:2px
    style D2 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
    style D3 fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
    style D4 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px

📂 Estrutura de Pastas e Arquivos

sistemas_python_intro_03_estoque_cantina/
├── degrau_1_imperativo_txt/
│   ├── main.py              # Script imperativo com menu e arquivo estoque.txt
│   └── test_degrau_1.py     # Testes automatizados de validação de estoque
├── degrau_2_primeira_classe/
│   ├── main.py              # Primeira classe POO (class Produto)
│   └── test_degrau_2.py     # Testes unitários da classe Produto
├── degrau_3_sqlite/
│   ├── main.py              # Banco relacional SQLite3 nativo com filtros
│   └── test_degrau_3.py     # Testes unitários de consultas e baixas SQL
├── degrau_4_interface_customtkinter/
│   └── main.py              # Aplicação Desktop com tabela, cards e alertas visuais
├── main.py                  # Hub Central / Launcher interativo
└── index.md                 # Este guia didático autoguiado

🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No VS Code / PyCharm, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd sistemas_python_intro_03_estoque_cantina

2. Execute o Hub Central ou um Degrau Específico

# Executar o Hub Central (Menu interativo com todos os degraus):
python main.py

# Ou executar um degrau individualmente:
python degrau_1_imperativo_txt/main.py
python degrau_2_primeira_classe/main.py
python degrau_3_sqlite/main.py
python degrau_4_interface_customtkinter/main.py

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta executar: python catalogo_projetos/sistemas_python_intro_03_estoque_cantina/main.py


📖 Roteiro Passo a Passo & Gabarito Completo


🪜 Degrau 1: Programação Imperativa com Arquivo de Texto (.txt)

No primeiro degrau, lemos e gravamos os itens no arquivo estoque.txt no formato nome,preco,quantidade. O programa impede vendas se a quantidade solicitada for maior que o saldo em estoque.

Código-Fonte Completo (degrau_1_imperativo_txt/main.py):

import os

ARQUIVO_PADRAO = "estoque.txt"

def carregar_estoque(caminho=ARQUIVO_PADRAO):
    produtos = []
    if not os.path.exists(caminho):
        return produtos

    with open(caminho, "r", encoding="utf-8") as f:
        for linha in f:
            linha = linha.strip()
            if linha:
                partes = linha.split(",")
                if len(partes) >= 3:
                    try:
                        produtos.append({"nome": partes[0], "preco": float(partes[1]), "quantidade": int(partes[2])})
                    except ValueError:
                        continue
    return produtos

def salvar_estoque(produtos, caminho=ARQUIVO_PADRAO):
    with open(caminho, "w", encoding="utf-8") as f:
        for p in produtos:
            f.write(f"{p['nome']},{p['preco']:.2f},{p['quantidade']}\n")

def cadastrar_produto(produtos, nome, preco, quantidade):
    nome = nome.strip()
    if not nome or preco <= 0 or quantidade < 0:
        return False
    produtos.append({"nome": nome, "preco": preco, "quantidade": quantidade})
    return True

def registrar_venda(produtos, nome, quantidade_vendida):
    for p in produtos:
        if p["nome"].lower() == nome.lower():
            if p["quantidade"] < quantidade_vendida:
                print(f"[ERRO] Estoque insuficiente! Disponível: {p['quantidade']} un.")
                return False
            p["quantidade"] -= quantidade_vendida
            print(f"[OK] Venda registrada! Total: R$ {p['preco'] * quantidade_vendida:.2f}")
            return True
    return False

Testes Automatizados (degrau_1_imperativo_txt/test_degrau_1.py):

"""
Testes Unitários para o Degrau 1 do Projeto 03 (Imperativo & Arquivo TXT)
"""

import os
import unittest
from main import (
    cadastrar_produto,
    carregar_estoque,
    registrar_venda,
    repor_estoque,
    salvar_estoque
)

ARQUIVO_TESTE = "teste_estoque.txt"


class TestDegrau1Estoque(unittest.TestCase):

    def setUp(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def tearDown(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def test_cadastrar_e_persistir_produtos(self):
        produtos = []
        cadastrar_produto(produtos, "Coxinha de Frango", 6.50, 20)
        cadastrar_produto(produtos, "Suco de Uva 300ml", 4.00, 15)

        self.assertEqual(len(produtos), 2)
        self.assertEqual(produtos[0]["preco"], 6.50)

        salvar_estoque(produtos, caminho=ARQUIVO_TESTE)
        carregados = carregar_estoque(caminho=ARQUIVO_TESTE)
        self.assertEqual(len(carregados), 2)
        self.assertEqual(carregados[0]["nome"], "Coxinha de Frango")
        self.assertEqual(carregados[1]["quantidade"], 15)

    def test_regras_de_venda_e_reposicao(self):
        produtos = [{"nome": "Empada", "preco": 7.00, "quantidade": 5}]

        # Venda com sucesso
        venda_ok = registrar_venda(produtos, "Empada", 3)
        self.assertTrue(venda_ok)
        self.assertEqual(produtos[0]["quantidade"], 2)

        # Tentativa de venda sem estoque suficiente
        venda_falha = registrar_venda(produtos, "Empada", 10)
        self.assertFalse(venda_falha)
        self.assertEqual(produtos[0]["quantidade"], 2)

        # Reposição de estoque
        repor_ok = repor_estoque(produtos, "Empada", 8)
        self.assertTrue(repor_ok)
        self.assertEqual(produtos[0]["quantidade"], 10)


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 2: Primeiro Contato com POO (Classe Produto)

No segundo degrau, a classe Produto reúne os atributos e as regras de negócio em métodos específicos como vender(), repor() e esta_baixo_estoque().

Código-Fonte Completo (degrau_2_primeira_classe/main.py):

class Produto:
    def __init__(self, nome, preco, quantidade=0):
        self.nome = nome
        self.preco = float(preco)
        self.quantidade = int(quantidade)

    def vender(self, qtd):
        if qtd <= 0:
            return False, "Quantidade inválida."
        if self.quantidade < qtd:
            return False, f"Estoque insuficiente ({self.quantidade} un disponíveis)."
        self.quantidade -= qtd
        return True, f"Venda de {qtd} un realizada! Total: R$ {self.preco * qtd:.2f}."

    def repor(self, qtd):
        if qtd > 0:
            self.quantidade += qtd
            return True, f"Estoque reposto (+{qtd} un). Total: {self.quantidade} un."
        return False, "Quantidade inválida."

    def esta_baixo_estoque(self, limite=5):
        return self.quantidade < limite

Testes Automatizados (degrau_2_primeira_classe/test_degrau_2.py):

"""
Testes Unitários para o Degrau 2 do Projeto 03 (Primeira Classe POO)
"""

import os
import unittest
from main import Produto, carregar_produtos, salvar_produtos

ARQUIVO_TESTE = "teste_estoque_poo.txt"


class TestDegrau2POO(unittest.TestCase):

    def setUp(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def tearDown(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def test_classe_produto_metodos(self):
        p = Produto("Suco de Laranja", 5.00, 10)
        self.assertFalse(p.esta_baixo_estoque())

        sucesso, _ = p.vender(7)
        self.assertTrue(sucesso)
        self.assertEqual(p.quantidade, 3)
        self.assertTrue(p.esta_baixo_estoque())  # 3 < 5

        # Tentativa de venda sem estoque
        falha, _ = p.vender(5)
        self.assertFalse(falha)
        self.assertEqual(p.quantidade, 3)

        # Reposição
        p.repor(12)
        self.assertEqual(p.quantidade, 15)
        self.assertFalse(p.esta_baixo_estoque())

    def test_persistencia_objetos(self):
        p1 = Produto("Pastel de Carne", 7.50, 8)
        p2 = Produto("Refrigerante Lata", 5.00, 2)

        salvar_produtos([p1, p2], caminho=ARQUIVO_TESTE)
        carregados = carregar_produtos(caminho=ARQUIVO_TESTE)

        self.assertEqual(len(carregados), 2)
        self.assertEqual(carregados[0].nome, "Pastel de Carne")
        self.assertEqual(carregados[1].quantidade, 2)
        self.assertTrue(carregados[1].esta_baixo_estoque())


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 3: Banco de Dados Relacional com SQLite3 Nativo

No terceiro degrau, criamos a tabela produtos no SQLite e executamos atualizações atômicas de estoque (UPDATE produtos SET quantidade = quantidade - ? WHERE id = ?).

Código-Fonte Completo (degrau_3_sqlite/main.py):

import sqlite3

BANCO_PADRAO = "estoque.db"

def inicializar_tabela(caminho=BANCO_PADRAO):
    conn = sqlite3.connect(caminho)
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nome TEXT NOT NULL UNIQUE,
            preco REAL NOT NULL,
            quantidade INTEGER NOT NULL DEFAULT 0
        )
    """)
    conn.commit()
    conn.close()

def registrar_venda(produto_id, quantidade_vendida, caminho=BANCO_PADRAO):
    conn = sqlite3.connect(caminho)
    cursor = conn.cursor()
    cursor.execute("SELECT preco, quantidade FROM produtos WHERE id = ?", (produto_id,))
    prod = cursor.fetchone()
    if not prod or prod[1] < quantidade_vendida:
        conn.close()
        return False

    cursor.execute("UPDATE produtos SET quantidade = quantidade - ? WHERE id = ?", (quantidade_vendida, produto_id))
    conn.commit()
    conn.close()
    return True

Testes Automatizados (degrau_3_sqlite/test_degrau_3.py):

"""
Testes Unitários para o Degrau 3 do Projeto 03 (SQLite3 Nativo)
"""

import os
import unittest
from main import (
    cadastrar_produto,
    inicializar_tabela,
    listar_produtos,
    registrar_venda,
    repor_estoque
)

BANCO_TESTE = "teste_estoque_degrau3.db"


class TestDegrau3SQLite(unittest.TestCase):

    def setUp(self):
        if os.path.exists(BANCO_TESTE):
            os.remove(BANCO_TESTE)
        inicializar_tabela(caminho=BANCO_TESTE)

    def tearDown(self):
        if os.path.exists(BANCO_TESTE):
            os.remove(BANCO_TESTE)

    def test_crud_e_regras_estoque_sqlite(self):
        p_id = cadastrar_produto("Misto Quente", 8.00, 10, caminho=BANCO_TESTE)
        self.assertIsNotNone(p_id)

        # Venda
        venda_ok = registrar_venda(p_id, 4, caminho=BANCO_TESTE)
        self.assertTrue(venda_ok)

        # Validação saldo restante
        prods = listar_produtos(caminho=BANCO_TESTE)
        self.assertEqual(prods[0][3], 6)

        # Reposição
        repor_ok = repor_estoque(p_id, 5, caminho=BANCO_TESTE)
        self.assertTrue(repor_ok)

        prods_depois = listar_produtos(caminho=BANCO_TESTE)
        self.assertEqual(prods_depois[0][3], 11)

    def test_filtro_estoque_critico(self):
        cadastrar_produto("Agua Mineral 500ml", 3.00, 2, caminho=BANCO_TESTE)
        cadastrar_produto("Pao de Queijo", 4.50, 20, caminho=BANCO_TESTE)

        criticos = listar_produtos(apenas_criticos=True, caminho=BANCO_TESTE)
        self.assertEqual(len(criticos), 1)
        self.assertEqual(criticos[0][1], "Agua Mineral 500ml")


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 4: Interface Gráfica Desktop com Alertas Visuais

No quarto degrau, criamos uma aplicação visual para cantina com formulário de cadastro, tabela interativa com ttk.Treeview, cards informativos no topo e tags de cor vermelha para destacar itens com estoque crítico (< 5 unidades).

Código-Fonte Completo (degrau_4_interface_customtkinter/main.py):

import sqlite3
import tkinter as tk
from tkinter import messagebox, ttk

class AppEstoqueCantina:
    def __init__(self, root):
        self.root = root
        self.root.title("🍔 Controle de Estoque da Cantina (Degrau 4)")
        self.root.geometry("640x580")
        self.criar_widgets()
        self.recarregar_dados()

    def recarregar_dados(self):
        for item in self.tree.get_children():
            self.tree.delete(item)
        produtos = obter_todos_produtos()
        for p_id, nome, preco, qtd in produtos:
            tag = "critico" if qtd < 5 else "normal"
            status = "⚠️ Crítico" if qtd < 5 else "✅ Normal"
            self.tree.insert("", "end", values=(f"#{p_id:02d}", nome, f"R$ {preco:.2f}", f"{qtd} un", status), tags=(tag,))

🎛️ Hub Central: Launcher Interativo (main.py)

O arquivo main.py na raiz do projeto funciona como um painel de controle: permite executar qualquer um dos 4 degraus (ou toda a suíte de testes) a partir de um único menu no terminal, sem precisar navegar manualmente entre as pastas.

Código-Fonte Completo (main.py):

"""
Hub Central / Launcher — Miniprojeto 03: Controle de Estoque da Cantina Escolar
Permite executar de forma autoguiada qualquer um dos 4 degraus pedagógicos ou rodar a suíte de testes.
"""

import os
import subprocess
import sys

BASE_DIR = os.path.dirname(os.path.abspath(__file__))


def rodar_script(caminho_relativo):
    caminho_absoluto = os.path.join(BASE_DIR, caminho_relativo)
    diretorio = os.path.dirname(caminho_absoluto)
    print(f"\n🚀 Executando: {caminho_relativo}...\n")
    try:
        subprocess.run([sys.executable, caminho_absoluto], cwd=diretorio, check=True)
    except KeyboardInterrupt:
        print("\n\n⏹️ Execucao interrompida pelo usuario.")
    except Exception as e:
        print(f"\n❌ Erro durante a execucao: {e}")


def rodar_todos_os_testes():
    print("\n🧪 ========================================================")
    print("   RODANDO A SUÍTE DE TESTES DOS DEGRAUS 1, 2 E 3")
    print("========================================================\n")
    testes = [
        ("Degrau 1 (Imperativo & Arquivo TXT)", "degrau_1_imperativo_txt/test_degrau_1.py"),
        ("Degrau 2 (Primeira Classe POO)", "degrau_2_primeira_classe/test_degrau_2.py"),
        ("Degrau 3 (Banco SQLite3 Nativo)", "degrau_3_sqlite/test_degrau_3.py"),
    ]
    for nome, arq in testes:
        print(f"▶️ Testando {nome}...")
        caminho_abs = os.path.join(BASE_DIR, arq)
        diretorio = os.path.dirname(caminho_abs)
        res = subprocess.run([sys.executable, caminho_abs], cwd=diretorio)
        if res.returncode == 0:
            print(f"✅ {nome}: PASSOU!\n")
        else:
            print(f"❌ {nome}: FALHOU!\n")


def menu_hub():
    while True:
        print("\n========================================================")
        print("🍔 MINIPROJETO 03: CONTROLE DE ESTOQUE DA CANTINA (HUB)")
        print("========================================================")
        print("1. 🪜 Executar Degrau 1 (Imperativo & Arquivo .txt)")
        print("2. 🪜 Executar Degrau 2 (Primeira Classe POO: class Produto)")
        print("3. 🪜 Executar Degrau 3 (Banco de Dados SQLite3 Nativo)")
        print("4. 🪜 Executar Degrau 4 (Interface Desktop Visual)")
        print("5. 🧪 Rodar Todos os Testes Automatizados (Degraus 1, 2 e 3)")
        print("6. 🚪 Sair")
        print("========================================================")
        opcao = input("Escolha o degrau que deseja executar (1-6): ").strip()

        if opcao == "1":
            rodar_script("degrau_1_imperativo_txt/main.py")
        elif opcao == "2":
            rodar_script("degrau_2_primeira_classe/main.py")
        elif opcao == "3":
            rodar_script("degrau_3_sqlite/main.py")
        elif opcao == "4":
            rodar_script("degrau_4_interface_customtkinter/main.py")
        elif opcao == "5":
            rodar_todos_os_testes()
        elif opcao == "6":
            print("👋 Encerrando o launcher. Bons estudos!")
            break
        else:
            print("❌ Opcao invalida. Digite de 1 a 6.")


if __name__ == "__main__":
    menu_hub()

🧪 Validação com Testes Automatizados

# Executar a suíte de testes do Miniprojeto 03:
python -c "import subprocess, sys; [subprocess.run([sys.executable, p], check=True) for p in ['catalogo_projetos/sistemas_python_intro_03_estoque_cantina/degrau_1_imperativo_txt/test_degrau_1.py', 'catalogo_projetos/sistemas_python_intro_03_estoque_cantina/degrau_2_primeira_classe/test_degrau_2.py', 'catalogo_projetos/sistemas_python_intro_03_estoque_cantina/degrau_3_sqlite/test_degrau_3.py']]"

# Ou, de forma ainda mais simples, use o Hub Central (opção 5 do menu):
python main.py

Resultado esperado:

Ran 2 tests in 0.006s OK (Degrau 1)
Ran 2 tests in 0.005s OK (Degrau 2)
Ran 2 tests in 0.085s OK (Degrau 3)


🏛️ Arquitetura Visual & Engenharia do Sistema

A imagem abaixo ilustra a esteira evolutiva dos 4 degraus didáticos do miniprojeto:

Arquitetura da Aplicação em 4 Degraus

flowchart TD
    subgraph S1["🪜 Degrau 1: Script Imperativo"]
        A1["Menu Terminal while"] --> A2["Arquivo de Texto/JSON"]
    end
    subgraph S2["🪜 Degrau 2: Orientação a Objetos"]
        B1["Modelo de Domínio POO"] --> B2["Encapsulamento de Regras"]
    end
    subgraph S3["🪜 Degrau 3: Banco Relacional"]
        C1["SQLite3 Local"] --> C2["Transações ACID & Agregações"]
    end
    subgraph S4["🪜 Degrau 4: Interface Visual"]
        D1["Aplicação Desktop / Web"] --> D2["UX Interativa Final"]
    end

    S1 --> S2
    S2 --> S3
    S3 --> S4

    style S1 fill:#fffde7,stroke:#fbc02d,stroke-width:1.5px
    style S2 fill:#e3f2fd,stroke:#1976d2,stroke-width:1.5px
    style S3 fill:#e8f5e9,stroke:#388e3c,stroke-width:1.5px
    style S4 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:1.5px

🎯 Engenharia de Requisitos & Modelagem Formal

👤 User Stories (Histórias de Usuário)

ID Persona História de Usuário Critério de Aceite Prioridade
US-01 Atendente da Cantina Desejo cadastrar lanches e bebidas com quantidade e preço no terminal Persistir registros com layout delimitado em estoque.txt Alta
US-02 Gerente da Cantina Desejo manipular produtos com métodos de baixa e reposição de estoque Classe Produto com validação de estoque mínimo e custo total Alta
US-03 Administrador Escolar Desejo banco SQLite com alertas para itens abaixo do ponto de pedido Queries SQL identificando produtos críticos via cláusulas WHERE Alta
US-04 Operador de Balcão Desejo interface desktop moderna com tema escuro e botões estilizados Janela CustomTkinter responsiva com cards visuais de produtos Alta
US-05 Auditor de Inventário Desejo suíte de testes que assegure prevenção de estoque negativo Validações de integridade nos métodos de saída de mercadorias Média

⚙️ Requisitos Funcionais (RF)

ID Descrição do Requisito Criticidade
RF-01 Leitura e escrita de estoque físico com controle de delimitadores em arquivo texto Alta
RF-02 Classe Produto com cálculo de margem de lucro e reposição de quantidade Alta
RF-03 Modelagem da tabela produtos com constraints NOT NULL e CHECK no SQLite3 Alta
RF-04 Interface visual moderna CustomTkinter com modo Dark/Light e feedback tátil Alta
RF-05 Launcher central e scripts de teste unitário para cada fase de evolução Média

🛡️ Requisitos Não-Funcionais (RNF)

ID Especificação Arquitetural Criticidade
RNF-01 Consistência de inventário atômica em transações concorrentes de venda Alta
RNF-02 Compatibilidade de renderização gráfica CustomTkinter em Windows e Linux Alta
RNF-03 Tempo de inicialização de UI inferior a 1 segundo Média
RNF-04 Código limpo e documentado com tipagem semântica em Python 3.8+ Média
RNF-05 Isolamento integral de dependências por degrau instrucional Alta

🔗 Matriz de Rastreabilidade (RTM)

User Story Requisito Funcional Requisito Não-Funcional Módulo / Artefato de Código Status de Homologação
US-01 RF-01 RNF-01, RNF-04 degrau_1_imperativo_txt/main.py Testado
US-02 RF-02 RNF-01, RNF-04 degrau_2_primeira_classe/main.py Testado
US-03 RF-03 RNF-01, RNF-05 degrau_3_sqlite/main.py Testado
US-04 RF-04 RNF-02, RNF-03 degrau_4_interface_customtkinter/main.py Testado
US-05 RF-05 RNF-01, RNF-05 main.py & test_degrau_*.py Testado

🏁 Checkpoints de Qualidade & Homologação

Checkpoint Escopo da Validação Artefato Verificado Status
CP-01 Script Imperativo & Persistência Inicial degrau_1_* PASS ✅
CP-02 Modelagem Orientada a Objetos degrau_2_* PASS ✅
CP-03 Integridade Relacional no SQLite degrau_3_* PASS ✅
CP-04 Interface Visual & Usabilidade degrau_4_* PASS ✅
CP-05 Hub Launcher Central & Suíte de Testes main.py + test_*.py PASS ✅