Ing. Bernardo Prado · UTH 2026-2 · DSM3
Construirás un Sistema de Inventario completo aplicando los principios SOLID de diseño, con interfaz gráfica usando Flet y base de datos MySQL para guardar, leer, actualizar y eliminar datos.
WAMP (Windows, Apache, MySQL, PHP) instala un servidor MySQL en tu computadora. Ve a wampserver.com, descarga la versión más reciente e instálala. Cuando esté corriendo, el ícono en la barra de tareas debe ser verde.
Busca el ícono de WAMP en la esquina inferior derecha de tu pantalla (barra de tareas). Debe ser verde. Si es amarillo o rojo, algún servicio no inició — haz clic derecho y selecciona "Start All Services".
mysql-connector-pythonEsta librería permite a Python comunicarse con MySQL. Abre una terminal en VS Code (Ctrl + `) y escribe (usaremos una versión fija para que todos en el grupo tengan exactamente el mismo comportamiento):
pip install mysql-connector-python==9.5.0
flet — Framework de GUIFlet permite crear interfaces gráficas (ventanas con botones, formularios, tablas) usando Python puro. No necesitas conocer HTML ni JavaScript. Instalaremos la versión 0.24.1 exacta — las versiones más nuevas de Flet renombraron varias cosas (por ejemplo ft.colors pasó a ft.Colors) y el código de esta práctica está escrito para 0.24.1.
pip install flet==0.24.1
flet ni mysql-connector-python sin el ==versión — una versión distinta puede usar nombres de atributos diferentes (como ft.Colors en vez de ft.colors) y el código de esta práctica dejará de funcionar.
Con WAMP corriendo (ícono verde), abre tu navegador y ve a http://localhost/phpmyadmin. Inicia sesión con usuario root y contraseña en blanco (vacía). Haz clic en "Nueva" (panel izquierdo) y crea una base de datos llamada inventario_uth. Usa cotejamiento utf8_general_ci.
En el panel izquierdo verás un botón "Nueva". Al hacer clic aparece un formulario. Escribe "inventario_uth" en el campo "Nombre de la base de datos" y haz clic en "Crear".
Crea la siguiente estructura de carpetas. En VS Code, crea una carpeta Unidad3 con esta organización:
SOLID son 5 principios de diseño de software orientado a objetos. Esta práctica los aplica todos. Léelos antes de escribir código.
Una clase debe tener una sola razón para cambiar. En nuestro proyecto: Producto solo conoce los datos, RepositorioProducto solo habla con la BD, y main.py solo maneja la interfaz.
Las clases deben estar abiertas para extender, cerradas para modificar. Si queremos agregar un nuevo tipo de repositorio (ej: guardar en archivo en vez de BD), creamos una nueva clase en lugar de modificar la existente.
Si B hereda de A, en cualquier lugar donde uses A puedes usar B sin que el programa se rompa. Visto en Unidad 2: EstudianteBecario puede usarse donde se espera un Estudiante.
No obligues a una clase a implementar métodos que no usa. Divide interfaces grandes en pequeñas. En Python esto se logra con múltiples clases abstractas pequeñas.
Las clases de alto nivel no deben depender de clases de bajo nivel — ambas deben depender de abstracciones. La GUI dependerá de una interfaz de repositorio, no de MySQL directamente.
La tienda "UTH Tech" necesita un sistema para controlar su inventario de productos electrónicos. El sistema debe:
Estos 4 pasos — Create, Read, Update, Delete — forman lo que se conoce como CRUD. Es la base de casi toda aplicación real.
| Variable/Atributo | Tipo | Ejemplo | Descripción |
|---|---|---|---|
id_producto | int | 1 | Clave primaria, auto-generada por MySQL |
nombre | str | "Laptop HP" | Nombre del producto |
categoria | str | "Computadoras" | Categoría del producto |
precio | float | 12500.00 | Precio unitario en pesos |
cantidad | int | 15 | Unidades disponibles en almacén |
configuracion.py)configuracion.pyEste archivo guarda los datos de conexión a la base de datos. Al tenerlos en un solo lugar, si cambian (nuevo servidor, contraseña, etc.) solo modificas aquí — no en todo el código. Esto aplica el principio DRY (Don't Repeat Yourself).
# =====================================================
# configuracion.py
# Datos de conexión a la base de datos MySQL
# =====================================================
# Diccionario con los datos de conexión
# Un diccionario es una estructura clave:valor en Python
CONFIGURACION_BD = { # Abre el diccionario de configuración de la BD
"host": "localhost", # Dirección del servidor MySQL (local)
"user": "root", # Usuario de MySQL (por defecto en WAMP)
"password": "", # Contraseña (vacía en WAMP por defecto)
"database": "inventario_uth" # Nombre de la base de datos
} # Cierra el diccionario CONFIGURACION_BD
# Nombre de la tabla que usaremos
TABLA_PRODUCTOS = "productos" # Constante con el nombre de la tabla SQL
| Fragmento | Explicación |
|---|---|
CONFIGURACION_BD = {...} | Un diccionario de Python. Las llaves van entre comillas ("host") y los valores también pueden ser strings, números, etc. Se accede con CONFIGURACION_BD["host"]. |
"localhost" | La dirección del servidor MySQL. Cuando usas WAMP localmente, siempre es "localhost" o "127.0.0.1". |
"password": "" | WAMP instala MySQL sin contraseña. Si tu MySQL tiene contraseña, escríbela aquí. |
modelos.py)Producto en modelos.pyLa clase Producto SOLO representa los datos de un producto. No sabe nada de bases de datos ni de interfaz gráfica. Esto es el principio de Responsabilidad Única (S de SOLID).
# =====================================================
# modelos.py
# Define la estructura de los datos (modelo Producto)
# SOLID: Responsabilidad Única — solo modela datos
# =====================================================
class Producto: # Declara la clase Producto (sin lógica de BD ni de GUI)
"""
Clase que modela un producto del inventario.
Solo representa datos — no hace operaciones de BD.
"""
def __init__(self, nombre: str, categoria: str, # Constructor: recibe los datos básicos del producto
precio: float, cantidad: int, id_producto: int = None): # id_producto es opcional (None = aún no guardado en MySQL)
"""
Constructor del Producto.
El id_producto es None al crear (MySQL lo asigna automáticamente).
"""
self.id_producto = id_producto # None = producto nuevo (sin ID aún)
self.nombre = nombre # Guarda el nombre recibido como atributo del objeto
self.categoria = categoria # Guarda la categoría recibida como atributo del objeto
self.precio = precio # Guarda el precio recibido como atributo del objeto
self.cantidad = cantidad # Guarda la cantidad recibida como atributo del objeto
def valor_total(self) -> float: # Método que calcula el valor total en inventario
"""Calcula el valor total del producto en inventario."""
return self.precio * self.cantidad # Multiplica precio por cantidad y retorna el resultado
def hay_stock(self) -> bool: # Método que indica si hay unidades disponibles
"""Retorna True si hay al menos 1 unidad disponible."""
return self.cantidad > 0 # True si la cantidad es mayor a cero
def __str__(self) -> str: # Define cómo se muestra el producto al imprimirlo
return ( # Inicia la construcción del string a retornar
f"[ID:{self.id_producto}] {self.nombre} | " # Primera parte: ID y nombre del producto
f"Categoría: {self.categoria} | " # Segunda parte: categoría del producto
f"Precio: ${self.precio:,.2f} | " # Tercera parte: precio formateado con 2 decimales
f"Stock: {self.cantidad}" # Cuarta parte: cantidad disponible en stock
) # Cierra la concatenación implícita de los strings y el return
def __repr__(self) -> str: # Define la representación técnica del objeto
"""Representación técnica del objeto (usada en debugging)."""
return f"Producto(id={self.id_producto}, nombre='{self.nombre}')" # String con formato Producto(id=.., nombre=..)
| Fragmento | Explicación |
|---|---|
id_producto: int = None | El ID tiene valor por defecto None. Cuando creamos un producto nuevo, aún no tiene ID — MySQL lo asignará al guardarlo. Después de guardar, el ID se actualiza. |
None | Es el valor nulo de Python. Significa "sin valor" o "no asignado todavía". |
self.precio * self.cantidad | El método valor_total() multiplica precio por cantidad para saber cuánto vale el stock total de ese producto. |
__repr__ | Similar a __str__ pero para uso técnico (debugging). Se muestra en la consola interactiva de Python. |
repositorio.py)Aquí va toda la lógica de comunicación con MySQL. Esta clase aplica SOLID: es la única responsable de la BD.
Primero definimos una interfaz abstracta que declara qué operaciones debe tener cualquier repositorio. Luego implementamos una para MySQL. Si en el futuro cambiamos a otra BD, creamos otra clase que implemente la misma interfaz.
# =====================================================
# repositorio.py
# Capa de acceso a datos: operaciones CRUD con MySQL
# SOLID: SRP + DIP (Inversión de dependencias)
# =====================================================
from abc import ABC, abstractmethod # clase base y decorador para crear interfaces
import mysql.connector # librería para conectar con MySQL
from mysql.connector import Error # clase de error de MySQL
from modelos import Producto # importamos nuestro modelo
from configuracion import CONFIGURACION_BD # datos de conexión
# ─── INTERFAZ ABSTRACTA (principio D de SOLID) ───
class IRepositorioProducto(ABC): # Hereda de ABC para volverse una clase abstracta
"""
Interfaz abstracta que define el contrato del repositorio.
Cualquier implementación DEBE implementar estos 4 métodos.
"""
@abstractmethod # Obliga a las subclases a implementar este método
def crear(self, producto: Producto) -> bool: pass # Firma del método para insertar un producto
@abstractmethod # Obliga a las subclases a implementar este método
def leer_todos(self) -> list: pass # Firma del método para listar todos los productos
@abstractmethod # Obliga a las subclases a implementar este método
def actualizar(self, producto: Producto) -> bool: pass # Firma del método para modificar un producto
@abstractmethod # Obliga a las subclases a implementar este método
def eliminar(self, id_producto: int) -> bool: pass # Firma del método para borrar un producto por ID
# ─── IMPLEMENTACIÓN CONCRETA PARA MYSQL ───
class RepositorioProductoMySQL(IRepositorioProducto): # Implementa la interfaz para MySQL
"""
Implementación del repositorio usando MySQL.
Solo esta clase sabe sobre MySQL — la GUI no necesita saberlo.
"""
def __init__(self): # Constructor: se ejecuta al crear el repositorio
"""Inicializa la conexión y crea la tabla si no existe."""
self.__conexion = None # Atributo privado que guardará la conexión a MySQL
self._conectar() # Llama al método interno que abre la conexión
self._crear_tabla() # Llama al método interno que crea la tabla si falta
def _conectar(self): # Método privado que abre la conexión con MySQL
"""Establece la conexión con la base de datos."""
try: # Intenta conectarse (puede fallar si MySQL no responde)
self.__conexion = mysql.connector.connect(**CONFIGURACION_BD) # Abre la conexión con los datos del diccionario
print("✅ Conexión a MySQL exitosa.") # Informa en consola que la conexión fue exitosa
except Error as e: # Captura cualquier error de conexión de MySQL
print(f"❌ Error al conectar: {e}") # Muestra el mensaje de error en consola
def _crear_tabla(self): # Método privado que crea la tabla 'productos' si falta
"""Crea la tabla 'productos' si no existe en la BD."""
sql = """
CREATE TABLE IF NOT EXISTS productos ( -- crea la tabla solo si aún no existe
id_producto INT AUTO_INCREMENT PRIMARY KEY, -- ID numérico autoincremental, clave primaria
nombre VARCHAR(100) NOT NULL, -- nombre del producto, obligatorio
categoria VARCHAR(60) NOT NULL, -- categoría del producto, obligatoria
precio DECIMAL(10,2) NOT NULL, -- precio con 2 decimales, obligatorio
cantidad INT NOT NULL DEFAULT 0 -- unidades en stock, 0 por defecto
) -- cierra la lista de columnas
""" # Fin de la sentencia SQL que crea la tabla
try: # Intenta ejecutar la creación de la tabla
cursor = self.__conexion.cursor() # Crea un cursor para ejecutar comandos SQL
cursor.execute(sql) # Ejecuta la sentencia CREATE TABLE
self.__conexion.commit() # Confirma el cambio en la base de datos
print("✅ Tabla 'productos' lista.") # Informa en consola que la tabla quedó lista
except Error as e: # Captura cualquier error al crear la tabla
print(f"❌ Error al crear tabla: {e}") # Muestra el mensaje de error en consola
# ────── CREATE ──────
def crear(self, producto: Producto) -> bool: # Método público para insertar un producto nuevo
"""INSERT: guarda un nuevo producto en la base de datos."""
sql = """
INSERT INTO productos (nombre, categoria, precio, cantidad) -- columnas a llenar
VALUES (%s, %s, %s, %s) -- placeholders seguros
""" # Fin de la sentencia SQL de inserción
# Los valores van en una tupla separada — NUNCA los pongas en el string
# (eso evita inyección SQL, un riesgo de seguridad)
valores = (producto.nombre, producto.categoria, # Tupla con nombre y categoría del producto
producto.precio, producto.cantidad) # Continúa la tupla con precio y cantidad
try: # Intenta ejecutar el INSERT (puede fallar)
cursor = self.__conexion.cursor() # Crea un cursor para ejecutar el INSERT
cursor.execute(sql, valores) # Ejecuta el INSERT sustituyendo los %s con la tupla
self.__conexion.commit() # Confirma la inserción en la base de datos
# Actualiza el ID del producto con el que MySQL asignó
producto.id_producto = cursor.lastrowid # Guarda el ID autogenerado en el objeto Producto
print(f"✅ Producto '{producto.nombre}' creado con ID {producto.id_producto}.") # Confirma en consola
return True # Indica que la operación fue exitosa
except Error as e: # Captura cualquier error al insertar
print(f"❌ Error al crear producto: {e}") # Muestra el mensaje de error en consola
return False # Indica que la operación falló
# ────── READ ──────
def leer_todos(self) -> list: # Método público para listar todos los productos
"""SELECT: retorna una lista con TODOS los productos."""
sql = "SELECT id_producto, nombre, categoria, precio, cantidad FROM productos ORDER BY nombre" # Consulta ordenada por nombre
try: # Intenta ejecutar la consulta (puede fallar)
cursor = self.__conexion.cursor() # Crea un cursor para ejecutar el SELECT
cursor.execute(sql) # Ejecuta la consulta SELECT
filas = cursor.fetchall() # obtiene todas las filas
# Convierte cada fila (tupla) en un objeto Producto
productos = [] # Lista vacía donde se acumularán los objetos Producto
for fila in filas: # Recorre cada fila (tupla) devuelta por MySQL
p = Producto( # Crea un objeto Producto a partir de la fila
id_producto = fila[0], # Columna 0: id_producto
nombre = fila[1], # Columna 1: nombre
categoria = fila[2], # Columna 2: categoría
precio = float(fila[3]), # Columna 3: precio convertido a float
cantidad = fila[4] # Columna 4: cantidad en stock
) # Cierra la creación del objeto Producto
productos.append(p) # Agrega el producto a la lista de resultados
return productos # Retorna la lista completa de productos
except Error as e: # Captura cualquier error al leer
print(f"❌ Error al leer productos: {e}") # Muestra el mensaje de error en consola
return [] # Retorna una lista vacía si falló la lectura
# ────── UPDATE ──────
def actualizar(self, producto: Producto) -> bool: # Método público para modificar un producto existente
"""UPDATE: modifica precio y cantidad de un producto existente."""
sql = """
UPDATE productos -- tabla a modificar
SET nombre = %s, categoria = %s, precio = %s, cantidad = %s -- nuevos valores
WHERE id_producto = %s -- fila a modificar
""" # Fin de la sentencia SQL de actualización
valores = (producto.nombre, producto.categoria, # Tupla con nombre y categoría actualizados
producto.precio, producto.cantidad, producto.id_producto) # Resto de valores y el ID a filtrar
try: # Intenta ejecutar el UPDATE (puede fallar)
cursor = self.__conexion.cursor() # Crea un cursor para ejecutar el UPDATE
cursor.execute(sql, valores) # Ejecuta el UPDATE sustituyendo los %s con la tupla
self.__conexion.commit() # Confirma la actualización en la base de datos
print(f"✅ Producto ID {producto.id_producto} actualizado.") # Confirma en consola
return True # Indica que la operación fue exitosa
except Error as e: # Captura cualquier error al actualizar
print(f"❌ Error al actualizar: {e}") # Muestra el mensaje de error en consola
return False # Indica que la operación falló
# ────── DELETE ──────
def eliminar(self, id_producto: int) -> bool: # Método público para borrar un producto por ID
"""DELETE: elimina un producto por su ID."""
sql = "DELETE FROM productos WHERE id_producto = %s" # Borra la fila cuyo ID coincida
try: # Intenta ejecutar el DELETE (puede fallar)
cursor = self.__conexion.cursor() # Crea un cursor para ejecutar el DELETE
cursor.execute(sql, (id_producto,)) # tupla de 1 elemento
self.__conexion.commit() # Confirma la eliminación en la base de datos
print(f"✅ Producto ID {id_producto} eliminado.") # Confirma en consola
return True # Indica que la operación fue exitosa
except Error as e: # Captura cualquier error al eliminar
print(f"❌ Error al eliminar: {e}") # Muestra el mensaje de error en consola
return False # Indica que la operación falló
def cerrar(self): # Método público para cerrar la conexión manualmente
"""Cierra la conexión con la base de datos (buena práctica)."""
if self.__conexion and self.__conexion.is_connected(): # Verifica que exista una conexión activa
self.__conexion.close() # Cierra la conexión con MySQL
print("🔌 Conexión cerrada.") # Informa en consola que la conexión se cerró
| Concepto SQL | Qué hace | Equivalente en Python |
|---|---|---|
INSERT INTO | Agrega una fila nueva a la tabla | Método crear() |
SELECT | Lee filas de la tabla | Método leer_todos() |
UPDATE SET | Modifica filas existentes | Método actualizar() |
DELETE WHERE | Elimina filas que cumplan condición | Método eliminar() |
%s en SQL | Placeholder seguro para valores | Se reemplaza con los valores de la tupla — previene inyección SQL |
cursor.commit() | Confirma los cambios en la BD | Sin esto, los cambios no se guardan permanentemente |
fetchall() | Obtiene todas las filas del resultado | Retorna una lista de tuplas |
sql = f"INSERT INTO productos VALUES ('{nombre}', ...)"
sql = "INSERT INTO productos VALUES (%s, ...)" + tupla de valores
main.py)Flet crea aplicaciones de escritorio con Python. Cada elemento visual (botón, campo de texto, tabla) es un control de Flet. La función principal recibe un objeto page que representa la ventana.
main.py — La interfaz gráfica completa# =====================================================
# main.py
# Interfaz gráfica con Flet — Sistema de Inventario
# =====================================================
import flet as ft # importamos Flet como 'ft'
from modelos import Producto # nuestro modelo de datos
from repositorio import RepositorioProductoMySQL # repositorio MySQL
def main(page: ft.Page): # Función de entrada que Flet invoca al iniciar la app
"""
Función principal de Flet.
'page' es la ventana principal de la aplicación.
"""
# ─── Configuración básica de la ventana ───
page.title = "📦 Inventario UTH Tech" # Texto que aparece en la barra de título
page.bgcolor = ft.colors.BLUE_GREY_50 # Color de fondo de toda la ventana
page.padding = 20 # Espacio interno entre el borde de la ventana y el contenido
page.window.width = 1000 # Ancho inicial de la ventana en píxeles
page.window.height = 700 # Alto inicial de la ventana en píxeles
# ─── Inicializar repositorio ───
repo = RepositorioProductoMySQL() # Crea el repositorio: abre conexión y crea la tabla
# ─── Variable para rastrear qué producto se está editando ───
producto_editando = [None] # lista de 1 elemento (mutable en closures)
# ────────────────────────────────────────────
# CONTROLES DEL FORMULARIO
# ────────────────────────────────────────────
campo_nombre = ft.TextField( # Crea el campo de texto para el nombre del producto
label="Nombre del producto", # Etiqueta visible sobre el campo
hint_text="Ej: Laptop HP 15", # Texto de ejemplo dentro del campo vacío
expand=True # El campo ocupa todo el espacio horizontal disponible
) # Cierra la creación de campo_nombre
campo_categoria = ft.TextField( # Crea el campo de texto para la categoría
label="Categoría", # Etiqueta visible sobre el campo
hint_text="Ej: Computadoras", # Texto de ejemplo dentro del campo vacío
expand=True # El campo ocupa todo el espacio horizontal disponible
) # Cierra la creación de campo_categoria
campo_precio = ft.TextField( # Crea el campo de texto para el precio
label="Precio ($)", # Etiqueta visible sobre el campo
hint_text="Ej: 12500.00", # Texto de ejemplo dentro del campo vacío
keyboard_type=ft.KeyboardType.NUMBER, # Muestra teclado numérico en móviles
expand=True # El campo ocupa todo el espacio horizontal disponible
) # Cierra la creación de campo_precio
campo_cantidad = ft.TextField( # Crea el campo de texto para la cantidad
label="Cantidad", # Etiqueta visible sobre el campo
hint_text="Ej: 15", # Texto de ejemplo dentro del campo vacío
keyboard_type=ft.KeyboardType.NUMBER, # Muestra teclado numérico en móviles
expand=True # El campo ocupa todo el espacio horizontal disponible
) # Cierra la creación de campo_cantidad
# Mensaje de estado (éxito o error)
lbl_estado = ft.Text("", size=14, color=ft.colors.GREEN_700) # Texto vacío al inicio, se llenará dinámicamente
# Tabla de datos
tabla = ft.DataTable( # Crea la tabla donde se listarán los productos
columns=[ # Abre la lista de columnas de la tabla
ft.DataColumn(ft.Text("ID", weight=ft.FontWeight.BOLD)), # Columna del ID
ft.DataColumn(ft.Text("Nombre", weight=ft.FontWeight.BOLD)), # Columna del nombre
ft.DataColumn(ft.Text("Categoría", weight=ft.FontWeight.BOLD)), # Columna de la categoría
ft.DataColumn(ft.Text("Precio", weight=ft.FontWeight.BOLD)), # Columna del precio
ft.DataColumn(ft.Text("Cantidad", weight=ft.FontWeight.BOLD)), # Columna de la cantidad
ft.DataColumn(ft.Text("Acciones", weight=ft.FontWeight.BOLD)), # Columna de botones editar/eliminar
], # Cierra la lista de columnas
rows=[], # Empieza sin filas; se llenarán al cargar los datos
bgcolor=ft.colors.WHITE, # Fondo blanco para la tabla
border_radius=8, # Bordes redondeados de la tabla
) # Cierra la creación de la tabla
# ────────────────────────────────────────────
# FUNCIONES DE LÓGICA
# ────────────────────────────────────────────
def limpiar_formulario(): # Función que deja el formulario listo para un nuevo producto
"""Vacía todos los campos del formulario."""
campo_nombre.value = "" # Borra el texto del campo nombre
campo_categoria.value = "" # Borra el texto del campo categoría
campo_precio.value = "" # Borra el texto del campo precio
campo_cantidad.value = "" # Borra el texto del campo cantidad
producto_editando[0] = None # Indica que ya no hay producto en edición
lbl_estado.value = "" # Borra el mensaje de estado mostrado
page.update() # Redibuja la página con los campos vacíos
def actualizar_tabla(): # Función que refresca la tabla con los datos actuales
"""Recarga y muestra todos los productos en la tabla."""
productos = repo.leer_todos() # Pide al repositorio la lista actual de productos
tabla.rows = [] # limpia las filas actuales
for p in productos: # Recorre cada producto para crear su fila en la tabla
# Capturamos 'p' con un parámetro por defecto para el closure
def editar(e, prod=p): # Manejador del botón editar para este producto
cargar_para_editar(prod) # Llena el formulario con los datos de prod
def borrar(e, prod=p): # Manejador del botón eliminar para este producto
repo.eliminar(prod.id_producto) # Borra el producto de la base de datos
lbl_estado.value = f"🗑️ '{prod.nombre}' eliminado." # Mensaje de confirmación
lbl_estado.color = ft.colors.ORANGE_700 # Color naranja para indicar eliminación
actualizar_tabla() # Vuelve a cargar la tabla sin el producto borrado
fila = ft.DataRow(cells=[ # Crea la fila de la tabla para este producto
ft.DataCell(ft.Text(str(p.id_producto))), # Celda con el ID
ft.DataCell(ft.Text(p.nombre)), # Celda con el nombre
ft.DataCell(ft.Text(p.categoria)), # Celda con la categoría
ft.DataCell(ft.Text(f"${p.precio:,.2f}")), # Celda con el precio formateado
ft.DataCell(ft.Text(str(p.cantidad))), # Celda con la cantidad
ft.DataCell(ft.Row([ # Celda con los botones de acción en fila
ft.IconButton( # Botón de ícono para editar
icon=ft.icons.EDIT, # Ícono de lápiz
tooltip="Editar", # Texto al pasar el cursor encima
icon_color=ft.colors.BLUE, # Color azul del ícono
on_click=editar # Llama a editar() al hacer clic
), # Cierra el botón de editar
ft.IconButton( # Botón de ícono para eliminar
icon=ft.icons.DELETE, # Ícono de bote de basura
tooltip="Eliminar", # Texto al pasar el cursor encima
icon_color=ft.colors.RED, # Color rojo del ícono
on_click=borrar # Llama a borrar() al hacer clic
), # Cierra el botón de eliminar
])), # Cierra la fila de botones y la celda de acciones
]) # Cierra la lista de celdas de la fila
tabla.rows.append(fila) # Agrega la fila recién creada a la tabla
page.update() # redibuja la página
def cargar_para_editar(producto: Producto): # Función que pasa los datos de un producto al formulario
"""Llena el formulario con los datos del producto a editar."""
producto_editando[0] = producto # Guarda referencia al producto que se está editando
campo_nombre.value = producto.nombre # Llena el campo nombre con el valor actual
campo_categoria.value = producto.categoria # Llena el campo categoría con el valor actual
campo_precio.value = str(producto.precio) # Llena el campo precio (convertido a texto)
campo_cantidad.value = str(producto.cantidad) # Llena el campo cantidad (convertido a texto)
lbl_estado.value = f"✏️ Editando: {producto.nombre}" # Mensaje indicando que se está editando
lbl_estado.color = ft.colors.BLUE_700 # Color azul para el mensaje de edición
page.update() # Redibuja la página con el formulario lleno
def guardar(e): # Manejador del botón Guardar (crea o actualiza)
"""Guarda (crea o actualiza) el producto del formulario."""
# Validar que los campos no estén vacíos
if not campo_nombre.value or not campo_precio.value: # Verifica que nombre y precio tengan valor
lbl_estado.value = "❌ Nombre y precio son obligatorios." # Mensaje de error de validación
lbl_estado.color = ft.colors.RED_700 # Color rojo para el mensaje de error
page.update() # Redibuja la página mostrando el error
return # Sale de la función sin guardar nada
try: # Intenta convertir los textos a número (puede fallar)
precio = float(campo_precio.value) # Convierte el texto del precio a número decimal
cantidad = int(campo_cantidad.value) if campo_cantidad.value else 0 # Convierte la cantidad o usa 0 si está vacía
except ValueError: # Captura el error si el texto no es un número válido
lbl_estado.value = "❌ Precio y cantidad deben ser números." # Mensaje de error de formato
lbl_estado.color = ft.colors.RED_700 # Color rojo para el mensaje de error
page.update() # Redibuja la página mostrando el error
return # Sale de la función sin guardar nada
if producto_editando[0] is None: # Comprueba si no hay producto en edición
# Modo CREAR (no hay producto en edición)
nuevo = Producto(campo_nombre.value, campo_categoria.value, precio, cantidad) # Crea el objeto Producto nuevo
repo.crear(nuevo) # Inserta el producto nuevo en la base de datos
lbl_estado.value = f"✅ '{nuevo.nombre}' guardado." # Mensaje de éxito al crear
else: # Si hay un producto siendo editado
# Modo ACTUALIZAR (hay un producto siendo editado)
p = producto_editando[0] # Obtiene el producto que se está editando
p.nombre = campo_nombre.value # Actualiza el nombre con el valor del formulario
p.categoria = campo_categoria.value # Actualiza la categoría con el valor del formulario
p.precio = precio # Actualiza el precio con el valor convertido
p.cantidad = cantidad # Actualiza la cantidad con el valor convertido
repo.actualizar(p) # Guarda los cambios del producto en la base de datos
lbl_estado.value = f"✅ '{p.nombre}' actualizado." # Mensaje de éxito al actualizar
lbl_estado.color = ft.colors.GREEN_700 # Color verde para el mensaje de éxito
limpiar_formulario() # Vacía el formulario para el siguiente registro
actualizar_tabla() # Refresca la tabla con los datos más recientes
# ────────────────────────────────────────────
# CONSTRUIR LA INTERFAZ
# ────────────────────────────────────────────
# Título de la aplicación
titulo = ft.Text( # Crea el texto grande del encabezado
"📦 Sistema de Inventario UTH Tech", # Texto del título
size=24, weight=ft.FontWeight.BOLD, # Tamaño grande y negrita
color=ft.colors.BLUE_900 # Color azul oscuro del texto
) # Cierra la creación del título
# Formulario
formulario = ft.Container( # Caja visual que agrupa todo el formulario
content=ft.Column([ # Organiza el contenido del formulario en columna
ft.Text("➕ Agregar / Editar Producto", size=16, weight=ft.FontWeight.BOLD), # Subtítulo del formulario
ft.Row([campo_nombre, campo_categoria]), # Fila con nombre y categoría lado a lado
ft.Row([campo_precio, campo_cantidad]), # Fila con precio y cantidad lado a lado
ft.Row([ # Fila que contiene los botones de acción
ft.ElevatedButton( # Botón principal para guardar
"💾 Guardar", # Texto visible del botón
bgcolor=ft.colors.BLUE_700, # Color de fondo azul del botón
color=ft.colors.WHITE, # Color blanco del texto del botón
on_click=guardar # Llama a guardar() al hacer clic
), # Cierra el botón Guardar
ft.OutlinedButton("🔄 Limpiar", on_click=lambda e: limpiar_formulario()), # Botón que vacía el formulario
]), # Cierra la fila de botones
lbl_estado, # Agrega el texto de estado al final del formulario
]), # Cierra la columna de contenido del formulario
padding=20, # Espacio interno alrededor del contenido
bgcolor=ft.colors.WHITE, # Fondo blanco del contenedor
border_radius=12, # Bordes redondeados del contenedor
shadow=ft.BoxShadow(blur_radius=8, color=ft.colors.BLUE_GREY_100), # Sombra suave alrededor
margin=ft.margin.only(bottom=16) # Espacio extra debajo del contenedor (ft.margin.only ya rellena left/top/right con 0)
) # Cierra la creación del formulario
# Agregar todo a la página
page.add( # Agrega los controles a la ventana, en orden
titulo, # Primero el título
formulario, # Luego el formulario completo
ft.Text("📋 Lista de Productos", size=16, weight=ft.FontWeight.BOLD), # Subtítulo de la lista
ft.Container( # Caja visual que envuelve la tabla
content=ft.Column([tabla], scroll=ft.ScrollMode.AUTO), # Columna con scroll que contiene la tabla
bgcolor=ft.colors.WHITE, # Fondo blanco del contenedor
border_radius=12, # Bordes redondeados del contenedor
padding=10, # Espacio interno alrededor de la tabla
shadow=ft.BoxShadow(blur_radius=8, color=ft.colors.BLUE_GREY_100), # Sombra suave alrededor
) # Cierra el contenedor de la tabla
) # Cierra page.add()
# Cargar datos al iniciar
actualizar_tabla() # Llena la tabla con los productos existentes al abrir la app
# Punto de entrada — Flet lanza la aplicación
ft.app(target=main) # Inicia la app de escritorio ejecutando la función main
| Control de Flet | ¿Para qué sirve? |
|---|---|
ft.Page | La ventana principal de la app. Configuras título, tamaño, fondo. |
ft.TextField | Campo de texto editable (como un input de HTML). |
ft.DataTable | Tabla de datos con filas y columnas. |
ft.ElevatedButton | Botón con relieve (el principal de acción). |
ft.IconButton | Botón con solo un ícono (editar, eliminar). |
ft.Row / ft.Column | Organizan controles en fila (horizontal) o columna (vertical). |
ft.Container | Caja con propiedades visuales: fondo, sombra, bordes redondeados. |
page.update() | Redibuja la pantalla después de cambios. SIEMPRE llamarlo al cambiar algo. |
ft.app(target=main) | Lanza la aplicación. target es la función que construye la interfaz. |
ft.colors.BLUE, ft.icons.EDIT. En versiones de Flet 0.25 o más nuevas estos cambiaron a mayúscula inicial (ft.Colors.BLUE, ft.Icons.EDIT). Como instalaste exactamente flet==0.24.1, usa siempre la forma en minúscula.
Antes de ejecutar, verifica que el ícono de WAMP en la barra de tareas sea verde. Si no está corriendo, abre WAMP Server desde el menú inicio.
Abre la terminal de VS Code (dentro de la carpeta Unidad3) y ejecuta:
python main.py
Se abrirá una ventana de escritorio con el formulario de agregar productos arriba y la tabla de productos abajo. Prueba agregar un producto: escribe nombre, categoría, precio y cantidad, luego haz clic en "💾 Guardar".
pip install mysql-connector-python==9.5.0 flet==0.24.1 ejecutado correctamenteinventario_uth creada en MySQLconfiguracion.py, modelos.py, repositorio.py, main.pyProducto en modelos.py no tiene código de BDIRepositorioProducto tiene 4 métodos abstractosRepositorioProductoMySQL