📚 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.
Trilha de Especialização — Nível Introdutório (Curso 25: Sistemas Desktop com Python)
- Visão Geral dos 10 Miniprojetos: 🌱 Guia Pedagógico de Miniprojetos
- Catálogo Geral: 🏗️ Catálogo de Projetos
- ➡️ Implementação Prática: Miniprojeto 07 (Código Completo em 4 Degraus)
Nas oficinas mecânicas de motos, carros e maquinários agrícolas de pequenas cidades, a composição de orçamentos somando peças substituídas com a mão de obra é essencial para transparência com o cliente.
Neste projeto, o estudante aprende como:
Peca e Orcamento encapsulando cálculos de subtotais, mão de obra e total geral.SUM(preco * quantidade).flowchart LR
D1["🪜 DEGRAU 1\nImperativo & JSON\nMenu while + orcamentos.json\ncom cálculo de peças + MO"] --> D2["🪜 DEGRAU 2\nPrimeira Classe POO\nClasses Peca e\nOrcamento (agregação)"]
D2 --> D3["🪜 DEGRAU 3\nSQLite Relacional (1:N)\nTabelas orcamentos e\nitens com agregação SUM"]
D3 --> D4["🪜 DEGRAU 4\nInterface Web Streamlit\nOrdem de serviço formatada,\nrecibo e aprovação"]
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
sistemas_python_intro_07_oficina_mecanica/
├── degrau_1_imperativo_json/
│ ├── main.py # Script imperativo com menu e arquivo orcamentos.json
│ └── test_degrau_1.py # Testes automatizados de cálculo de orçamento
├── degrau_2_primeira_classe/
│ ├── main.py # Classes POO (class Peca e class Orcamento)
│ └── test_degrau_2.py # Testes unitários de agregação e cálculos
├── degrau_3_sqlite/
│ ├── main.py # SQLite3 relacional: 2 tabelas e agregação ponderada SUM
│ └── test_degrau_3.py # Testes unitários de cálculo SQL e aprovação
├── degrau_4_interface_streamlit/
│ ├── app.py # Painel Web em Python puro com Streamlit (Ordem de Serviço)
│ └── main.py # Launcher do servidor Web Streamlit
├── main.py # Hub Central / Launcher interativo
└── index.md # Este guia didático autoguiado
No VS Code / PyCharm, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd sistemas_python_intro_07_oficina_mecanica
# Executar o Hub Central (Menu interativo com todos os degraus):
python main.py
# Ou executar os degraus de terminal (1 a 3):
python degrau_1_imperativo_json/main.py
python degrau_2_primeira_classe/main.py
python degrau_3_sqlite/main.py
# Executar a aplicação Web (Degrau 4 com Streamlit):
streamlit run degrau_4_interface_streamlit/app.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_07_oficina_mecanica/main.py
.json)No primeiro degrau, armazenamos os orçamentos e suas respectivas listas de peças em orcamentos.json.
degrau_1_imperativo_json/main.py):import json
import os
ARQUIVO_PADRAO = "orcamentos.json"
def criar_orcamento(orcamentos, cliente, veiculo, mao_de_obra=0.0):
if not cliente.strip() or not veiculo.strip():
return None
novo = {
"id": len(orcamentos) + 1,
"cliente": cliente.strip(),
"veiculo": veiculo.strip(),
"mao_de_obra": float(mao_de_obra),
"pecas": [],
"status": "ABERTO"
}
orcamentos.append(novo)
return novo
def adicionar_peca(orcamento, nome_peca, preco_unitario, quantidade=1):
orcamento["pecas"].append({
"nome": nome_peca.strip(),
"preco_unitario": float(preco_unitario),
"quantidade": int(quantidade),
"subtotal": float(preco_unitario * quantidade)
})
return True
def calcular_total_orcamento(orcamento):
tot_pecas = sum(p["subtotal"] for p in orcamento["pecas"])
return tot_pecas, orcamento["mao_de_obra"], (tot_pecas + orcamento["mao_de_obra"])
degrau_1_imperativo_json/test_degrau_1.py):"""
Testes Unitários para o Degrau 1 do Projeto 07 (Imperativo & JSON)
"""
import os
import unittest
from main import (
adicionar_peca,
calcular_total_orcamento,
carregar_orcamentos,
criar_orcamento,
salvar_orcamentos
)
ARQUIVO_TESTE = "teste_orcamentos.json"
class TestDegrau1Oficina(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_criar_orcamento_e_calculos(self):
orcamentos = []
orc = criar_orcamento(orcamentos, "Marcos Silva", "Gol 1.6 / ABC-1234", mao_de_obra=150.00)
self.assertIsNotNone(orc)
self.assertEqual(len(orcamentos), 1)
# Adicionar peças
adicionar_peca(orc, "Amortecedor Dianteiro", 220.00, 2)
adicionar_peca(orc, "Bieleta", 45.00, 2)
tot_p, mo, tot_g = calcular_total_orcamento(orc)
self.assertEqual(tot_p, 530.00) # 2*220 + 2*45 = 440 + 90 = 530
self.assertEqual(mo, 150.00)
self.assertEqual(tot_g, 680.00) # 530 + 150 = 680
# Salvamento JSON
salvar_orcamentos(orcamentos, caminho=ARQUIVO_TESTE)
carregados = carregar_orcamentos(caminho=ARQUIVO_TESTE)
self.assertEqual(len(carregados), 1)
self.assertEqual(carregados[0]["cliente"], "Marcos Silva")
self.assertEqual(len(carregados[0]["pecas"]), 2)
if __name__ == "__main__":
unittest.main()
Peca e Orcamento)No segundo degrau, a classe Peca calcula seu próprio subtotal e a classe Orcamento reúne as peças calculando o valor final com mão de obra.
degrau_2_primeira_classe/main.py):class Peca:
def __init__(self, nome, preco_unitario, quantidade=1):
self.nome = nome.strip()
self.preco_unitario = float(preco_unitario)
self.quantidade = int(quantidade)
def subtotal(self):
return self.preco_unitario * self.quantidade
class Orcamento:
def __init__(self, id_orc, cliente, veiculo, mao_de_obra=0.0, status="ABERTO"):
self.id = int(id_orc)
self.cliente = cliente.strip()
self.veiculo = veiculo.strip()
self.mao_de_obra = float(mao_de_obra)
self.status = status
self.pecas = []
def adicionar_peca(self, peca):
self.pecas.append(peca)
return True
def total_pecas(self):
return sum(p.subtotal() for p in self.pecas)
def total_geral(self):
return self.total_pecas() + self.mao_de_obra
degrau_2_primeira_classe/test_degrau_2.py):"""
Testes Unitários para o Degrau 2 do Projeto 07 (Primeira Classe POO)
"""
import os
import unittest
from main import GerenciadorOrcamentos, Orcamento, Peca
ARQUIVO_TESTE = "teste_orcamentos_poo.json"
class TestDegrau2Oficina(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_peca_e_orcamento(self):
p1 = Peca("Disco de Freio", 180.00, 2)
self.assertEqual(p1.subtotal(), 360.00)
p2 = Peca("Fluido de Freio DOT4", 35.00, 1)
self.assertEqual(p2.subtotal(), 35.00)
orc = Orcamento(1, "Renata Souza", "Civic 2.0 / XYZ-9988", mao_de_obra=120.00)
orc.adicionar_peca(p1)
orc.adicionar_peca(p2)
self.assertEqual(orc.total_pecas(), 395.00)
self.assertEqual(orc.total_geral(), 515.00)
self.assertEqual(orc.status, "ABERTO")
orc.aprovar()
self.assertEqual(orc.status, "APROVADO")
def test_gerenciador_persistencia(self):
ger1 = GerenciadorOrcamentos()
o = ger1.criar_orcamento("Paulo Sergio", "Uno Mille", 80.00)
o.adicionar_peca(Peca("Cabo de Vela", 90.00, 1))
ger1.salvar(caminho=ARQUIVO_TESTE)
ger2 = GerenciadorOrcamentos()
ger2.carregar(caminho=ARQUIVO_TESTE)
self.assertEqual(len(ger2.orcamentos), 1)
self.assertEqual(ger2.orcamentos[0].cliente, "Paulo Sergio")
self.assertEqual(ger2.orcamentos[0].total_geral(), 170.00)
if __name__ == "__main__":
unittest.main()
No terceiro degrau, criamos as tabelas relacionais orcamentos e itens_orcamento calculando os valores diretamente no SQL.
degrau_3_sqlite/main.py):import sqlite3
def inicializar_tabelas(caminho="oficina.db"):
conn = sqlite3.connect(caminho)
conn.execute("PRAGMA foreign_keys = ON;")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS orcamentos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cliente TEXT NOT NULL,
veiculo TEXT NOT NULL,
mao_de_obra REAL DEFAULT 0.0,
data_criacao TEXT DEFAULT (date('now', 'localtime')),
status TEXT DEFAULT 'ABERTO' CHECK(status IN ('ABERTO', 'APROVADO', 'REJEITADO'))
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS itens_orcamento (
id INTEGER PRIMARY KEY AUTOINCREMENT,
orcamento_id INTEGER NOT NULL,
nome_peca TEXT NOT NULL,
preco_unitario REAL NOT NULL,
quantidade INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (orcamento_id) REFERENCES orcamentos(id) ON DELETE CASCADE
)
""")
conn.commit()
conn.close()
def listar_orcamentos_com_totais(caminho="oficina.db"):
conn = sqlite3.connect(caminho)
cursor = conn.cursor()
cursor.execute("""
SELECT
o.id, o.cliente, o.veiculo, o.mao_de_obra, o.status, o.data_criacao,
COALESCE(SUM(i.preco_unitario * i.quantidade), 0.0) AS total_pecas,
(o.mao_de_obra + COALESCE(SUM(i.preco_unitario * i.quantidade), 0.0)) AS total_geral
FROM orcamentos o
LEFT JOIN itens_orcamento i ON o.id = i.orcamento_id
GROUP BY o.id, o.cliente, o.veiculo, o.mao_de_obra, o.status, o.data_criacao
ORDER BY o.id ASC
""")
linhas = cursor.fetchall()
conn.close()
return linhas
degrau_3_sqlite/test_degrau_3.py):"""
Testes Unitários para o Degrau 3 do Projeto 07 (SQLite3 Relacional com Agregações)
"""
import os
import unittest
from main import (
adicionar_item,
aprovar_orcamento,
criar_orcamento,
inicializar_tabelas,
listar_orcamentos_com_totais
)
BANCO_TESTE = "teste_oficina_degrau3.db"
class TestDegrau3SQLite(unittest.TestCase):
def setUp(self):
if os.path.exists(BANCO_TESTE):
os.remove(BANCO_TESTE)
inicializar_tabelas(caminho=BANCO_TESTE)
def tearDown(self):
if os.path.exists(BANCO_TESTE):
os.remove(BANCO_TESTE)
def test_fluxo_orcamento_e_calculo_sql(self):
# 1. Cria orçamento com mão de obra de R$ 150.00
orc_id = criar_orcamento("Roberto Carlos", "Corolla 2018 / ABC-1234", 150.00, caminho=BANCO_TESTE)
self.assertIsNotNone(orc_id)
# 2. Adiciona peças
adicionar_item(orc_id, "Jogo de Velas Iridium", 120.00, 1, caminho=BANCO_TESTE)
adicionar_item(orc_id, "Filtro de Ar", 40.00, 2, caminho=BANCO_TESTE)
# 3. Valida cálculo SQL (120 + 2*40 = 200 de peças + 150 MO = 350 total)
lista = listar_orcamentos_com_totais(caminho=BANCO_TESTE)
self.assertEqual(len(lista), 1)
self.assertEqual(lista[0][6], 200.00) # total_pecas
self.assertEqual(lista[0][7], 350.00) # total_geral
self.assertEqual(lista[0][4], "ABERTO")
# 4. Aprovação
aprovado = aprovar_orcamento(orc_id, caminho=BANCO_TESTE)
self.assertTrue(aprovado)
lista_depois = listar_orcamentos_com_totais(caminho=BANCO_TESTE)
self.assertEqual(lista_depois[0][4], "APROVADO")
if __name__ == "__main__":
unittest.main()
No quarto degrau, o aluno cria uma aplicação web interativa com formulário de cadastro, inclusão dinâmica de peças e visualização de recibo com botões de aprovação.
degrau_4_interface_streamlit/app.py):import streamlit as st
def main():
st.set_page_config(page_title="Orçamentos Oficina Mecânica", page_icon="🔧", layout="wide")
st.title("🔧 Gestão de Orçamentos — Oficina Mecânica")
# Métricas
col1, col2, col3 = st.columns(3)
col1.metric("📋 Total de Orçamentos", "8")
col2.metric("✅ Orçamentos Aprovados", "6/8")
col3.metric("💰 Faturamento Aprovado", "R$ 4.250,00")
degrau_4_interface_streamlit/main.py):"""
Launcher para o Degrau 4 (Streamlit Web)
Verifica se o pacote streamlit está instalado e inicia a aplicação no navegador.
"""
import os
import subprocess
import sys
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
APP_FILE = os.path.join(BASE_DIR, "app.py")
def main():
print("\n========================================================")
print("🌐 INICIANDO SISTEMA DE ORÇAMENTOS NA WEB (STREAMLIT)")
print("========================================================\n")
try:
import streamlit # noqa: F401
print("✅ Streamlit detectado! Abrindo a aplicacao no seu navegador padrao...\n")
cmd = [sys.executable, "-m", "streamlit", "run", APP_FILE]
subprocess.run(cmd, cwd=BASE_DIR)
except ImportError:
print("⚠️ O pacote 'streamlit' nao foi encontrado no ambiente Python atual.")
print("\nPara instalar e rodar, execute no seu terminal:")
print(" pip install streamlit")
print(f" streamlit run \"{APP_FILE}\"\n")
if __name__ == "__main__":
main()
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.
main.py):"""
Hub Central / Launcher — Miniprojeto 07: Orçamento de Oficina Mecânica
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 JSON)", "degrau_1_imperativo_json/test_degrau_1.py"),
("Degrau 2 (Primeira Classe POO)", "degrau_2_primeira_classe/test_degrau_2.py"),
("Degrau 3 (Banco SQLite3 Relacional)", "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 07: OFICINA MECÂNICA (HUB)")
print("========================================================")
print("1. 🪜 Executar Degrau 1 (Imperativo & Arquivo .json)")
print("2. 🪜 Executar Degrau 2 (Primeira Classe POO: Peca e Orcamento)")
print("3. 🪜 Executar Degrau 3 (Banco de Dados SQLite3 Relacional)")
print("4. 🪜 Executar Degrau 4 (Interface Web com Streamlit)")
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_json/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_streamlit/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()
# Executar a suíte de testes do Miniprojeto 07:
python -c "import subprocess, sys; [subprocess.run([sys.executable, p], check=True) for p in ['catalogo_projetos/sistemas_python_intro_07_oficina_mecanica/degrau_1_imperativo_json/test_degrau_1.py', 'catalogo_projetos/sistemas_python_intro_07_oficina_mecanica/degrau_2_primeira_classe/test_degrau_2.py', 'catalogo_projetos/sistemas_python_intro_07_oficina_mecanica/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 obtido:
Ran 1 test in 0.003s OK (Degrau 1)
Ran 2 tests in 0.005s OK (Degrau 2)
Ran 1 test in 0.065s OK (Degrau 3)
A imagem abaixo ilustra a esteira evolutiva dos 4 degraus didáticos do miniprojeto:
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
| ID | Persona | História de Usuário | Critério de Aceite | Prioridade |
|---|---|---|---|---|
| US-01 | Mecânico Responsável | Desejo cadastrar veículo, defeito relatado e peças em arquivo JSON | Gravar orçamento inicial com status de aprovação em orcamentos.json | Alta |
| US-02 | Chefe de Oficina | Desejo calcular custo de peças e mão de obra através de classe POO | Classe OrdemServico com cálculo de total, desconto e comissão | Alta |
| US-03 | Proprietário da Oficina | Desejo persistir ordens no SQLite para acompanhar faturamento mensal | Tabelas de clientes, veículos e itens de serviço com joins SQL | Alta |
| US-04 | Consultor Técnico | Desejo painel web Streamlit para emitir orçamentos e aprovar serviços | Interface web com formulário de itens, cálculo instantâneo e impressão | Alta |
| US-05 | Auditor de Processos | Desejo testes que garantam a consistência das somas de peças e horas | Testes automatizados cobrindo cenários com e sem desconto | Média |
| ID | Descrição do Requisito | Criticidade |
|---|---|---|
| RF-01 | Estruturação de ordens de serviço com listas aninhadas de itens em JSON | Alta |
| RF-02 | Encapsulamento de regras tributárias e cálculos de mão de obra em POO | Alta |
| RF-03 | Persistência relacional em SQLite com suporte a múltiplas peças por ordem | Alta |
| RF-04 | Aplicação web Streamlit com painel de indicadores e emissão de OS | Alta |
| RF-05 | Launcher central e rotinas automatizadas de teste em ambiente isolado | Média |
| ID | Especificação Arquitetural | Criticidade |
|---|---|---|
| RNF-01 | Rigor aritmético com duas casas decimais em todos os montantes financeiros | Alta |
| RNF-02 | Carregamento instantâneo de orçamentos históricos no dashboard | Média |
| RNF-03 | Interface clara e autoexplicativa com validações em tempo real | Média |
| RNF-04 | Independência de servidores de banco de dados externos pesados | Alta |
| RNF-05 | Separação pedagógica estrita entre os 4 degraus instrucionais | Alta |
| 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_json/main.py |
Testado |
| US-02 | RF-02 |
RNF-01, RNF-05 |
degrau_2_primeira_classe/main.py |
Testado |
| US-03 | RF-03 |
RNF-01, RNF-04 |
degrau_3_sqlite/main.py |
Testado |
| US-04 | RF-04 |
RNF-02, RNF-03 |
degrau_4_interface_streamlit/app.py |
Testado |
| US-05 | RF-05 |
RNF-01, RNF-05 |
main.py & test_degrau_*.py |
Testado |
| 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 ✅ |