Ya tenemos login y registro. Hoy completamos TODO el sistema: los archivos de datos JSON, los servicios de productos, ventas y administración, y los menús que conectan todo. Al final entenderás el proyecto completo — y estarás listo para construir el tuyo.
file_manager.py y auth_service.py. Hoy vamos a completar todos los archivos que faltan. Pero antes, mira el mapa completo del proyecto terminado. Fíjate en las flechas mentales: cada capa solo habla con la capa de abajo. 🗺️
menus y services son "carpetas de código"? ¿No son carpetas normales?
from menus.menu_principal import …, Python busca una carpeta llamada menus a partir de donde ejecutaste el programa (por eso siempre corremos desde main.py). El punto en menus.menu_principal se lee como: carpeta menus → archivo menu_principal.py. Es la dirección postal del código. 📬
import siempre apuntan hacia abajo. main.py importa de menus/. Los menús importan de services/. Los servicios importan de utils/. Y solo utils/file_manager.py toca los archivos de data/. Nunca al revés: un servicio jamás importa un menú. Si respetas esto, tu proyecto (CRM, LMS o PQRS) será fácil de arreglar cuando algo falle.file_manager.py y auth_service.py de la clase pasada funcionandousuarios.json, productos.json, ventas.json
Nuestra "base de datos" son 3 archivos de texto
[ ] y diccionarios con { }. ¿Te suenan? ¡Son las mismas estructuras de Python! 📄
guardar_json crea el archivo si no existe). Pero necesitas entender su estructura, porque todo el código que escribiremos hoy lee y modifica estos archivos. Míralos como el "plano" de tus datos. 🏗️
[ ← ① El archivo completo es UNA LISTA { ← ② Cada usuario es UN DICCIONARIO "id": 1, ← ③ número, SIN comillas "nombre": "juan", "correo": "juan@gmail.com", "password": "1234", ← texto, CON comillas "rol": "vendedor" }, ← ④ coma separa los elementos { "id": 2, "nombre": "maria", "correo": "maria@gmail.com", "password": "abcd", "rol": "comprador" } ]
[ { "id": 1, "nombre": "Audífonos Bluetooth", "categoria": "Tecnología", "precio": 85000.0, ← decimal (float en Python) "stock": 10, ← entero (int en Python) "id_vendedor": 1 ← ⑤ ¡La conexión con usuarios.json! } ]
[ { "id": 1, "id_comprador": 2, ← apunta al usuario con id 2 "nombre_comprador": "maria", "productos": [ ← ⑥ ¡una lista DENTRO del diccionario! { "id_producto": 1, "nombre": "Audífonos Bluetooth", "cantidad": 2, "precio_unitario": 85000.0, "subtotal": 170000.0 } ], "total": 170000.0, "fecha": "2026-07-02 10:30:00" } ]
[ ] — Por eso leer_json() devuelve [] cuando el archivo no existe: el resto del código siempre espera una lista, tenga 0 o 100 elementos. Un for sobre una lista vacía simplemente no hace nada — no explota.{ } — Pares "clave": valor. En Python accedes con corchetes: usuario["nombre"] devuelve "juan". Si escribes mal la clave (usuario["Nombre"] con mayúscula) Python lanza KeyError: las claves distinguen mayúsculas."juan"), números sin comillas (1, 85000.0). Cuando json.load() lee el archivo, convierte cada valor a su tipo Python: str, int, float. Por eso podemos hacer matemáticas con precio sin convertir nada.json.load() lanza JSONDecodeError (y gracias a nuestro try/except del file_manager, el sistema devuelve [] en vez de morir… ¡pero pierdes de vista tus datos! Cuidado)."id_vendedor": 1 — la relación entre archivos — El producto NO guarda el nombre del vendedor: guarda su id. Es como una cédula: con el id puedes ir a usuarios.json y encontrar todos sus datos. En bases de datos reales esto se llama llave foránea. Tu proyecto necesitará lo mismo: en un CRM, la interacción guarda el id_cliente; en un LMS, la nota guarda el id_estudiante; en un PQRS, la respuesta guarda el id_peticion.venta["productos"][0]["cantidad"]. Se lee de izquierda a derecha: entra a la venta → toma la lista productos → toma el elemento 0 → toma su cantidad."data/usuarios.json". Windows no distingue Usuarios.json de usuarios.json, pero Linux y Mac SÍ. Acostúmbrate desde ya: rutas y nombres de archivo en minúsculas, sin tildes y sin espacios. Te ahorrará dolores de cabeza en el demo day.id_vendedor en productos.json (relación entre archivos)data/usuarios.json desde la clase pasada y reconocí la estructuraventa["productos"][0]["subtotal"]main.py (versión final)
El punto de entrada: 4 líneas que arrancan todo
main.py solo hace una cosa: arrancar el menú principal. Toda la lógica vive en las otras capas. Un buen main.py es como el botón de encendido de un carro: pequeño, pero sin él nada arranca. 🔑
# ① Importamos la función que dibuja el menú principal from menus.menu_principal import mostrar_menu_principal # ② Este if es el "portero" del programa if __name__ == "__main__": mostrar_menu_principal() # ③ ← estas 4 espacios de indentación son OBLIGATORIOS
from menus.menu_principal import mostrar_menu_principal — Se lee: "de la carpeta menus, del archivo menu_principal.py, tráeme la función mostrar_menu_principal". Nota que en el import NO se escribe el .py ni los paréntesis de la función: importas el nombre, no la ejecutas todavía.if __name__ == "__main__": — Python le pone a cada archivo una variable secreta llamada __name__. Si ejecutas el archivo directamente (botón ▶ en Spyder), vale "__main__" y el código de adentro corre. Si otro archivo lo importa, vale otra cosa y NO corre. Así evitas que el programa se ejecute "por accidente" al ser importado. Son dos guiones bajos a cada lado: __name__, no _name_.FinSi para cerrar un bloque. Python no tiene "Fin": el bloque se define por los 4 espacios a la izquierda. Todo lo que esté indentado bajo el if pertenece al if. Si quitas la indentación, la línea queda "afuera". Si mezclas espacios y tabulaciones, Python lanza IndentationError. Spyder pone los 4 espacios solo al presionar Tab — déjalo trabajar.main.py? Dos razones. Primera: las rutas "data/usuarios.json" son relativas a la carpeta desde donde ejecutas — si corres producto_service.py directo, buscará data/ dentro de services/ y fallará. Segunda: los imports from services... se resuelven desde la raíz del proyecto. En Spyder: abre main.py y presiona F5.mostrar_menu_principal en main.pyif __name__ == "__main__": con los dobles guiones bajosmenus/menu_principal.py
El "recepcionista": recibe a todos y los envía a su menú según el rol
iniciar_sesion() devuelve el diccionario del usuario o None? Hoy por fin usamos ese valor: el menú principal mira el campo "rol" y decide a cuál de los 3 menús enviarte. Es un enrutador: como el recepcionista de un edificio que te dice a qué piso subir. 🛎️
# ① Importamos de las DOS capas que este menú necesita from services.auth_service import registrar_usuario, iniciar_sesion from menus.menu_admin import menu_admin from menus.menu_vendedor import menu_vendedor from menus.menu_comprador import menu_comprador from services.producto_service import listar_productos def mostrar_menu_principal(): while True: # ② ciclo infinito: el menú se repite hasta el break print("\n====================================") print(" PYMARKET CONSOLE") print("====================================") print("1. Registrarse") print("2. Iniciar sesión") print("3. Ver productos disponibles") print("4. Salir") opcion = input("Seleccione una opción: ") if opcion == "1": registrar_usuario() elif opcion == "2": usuario = iniciar_sesion() # ③ recibimos el dict… o None if usuario: # ④ None se evalúa como False if usuario["rol"] == "admin": menu_admin(usuario) # ⑤ ¡le PASAMOS el usuario al menú! elif usuario["rol"] == "vendedor": menu_vendedor(usuario) elif usuario["rol"] == "comprador": menu_comprador(usuario) else: print("Credenciales incorrectas.") elif opcion == "3": listar_productos() # cualquiera puede ver productos sin login elif opcion == "4": print("Gracias por usar PyMarket Console.") break # ⑥ única salida del while True else: print("Opción no válida.")
while True: — En PSEINT usabas Repetir … Hasta Que. Aquí while True es un ciclo que en principio nunca termina: muestra el menú, procesa la opción, y vuelve a empezar. La única forma de salir es la instrucción break. Sin este ciclo, el programa mostraría el menú una vez y se cerraría.usuario = iniciar_sesion() — Aquí está la magia de las capas: la función vive en auth_service.py, pero su valor de retorno viaja hasta acá y lo guardamos en una variable local. El dato cruzó de la capa de servicios a la capa de interfaz.if usuario: — En Python, None se comporta como False dentro de un if, y un diccionario con datos como True. Es un atajo de if usuario is not None:. Ambas formas son válidas; elige una y sé consistente.menu_admin(usuario) — pasar datos como argumento — Le entregamos el diccionario completo al menú. Así, dentro de menu_vendedor podremos saludar con usuario['nombre'] y filtrar productos con usuario['id']. El usuario logueado "viaja" por todo el sistema como argumento de función.break — Rompe el while inmediatamente. Como es la última pantalla del programa, al salir del ciclo la función termina, main.py termina, y el programa se cierra limpiamente.if usuario["rol"] == "admin": está DENTRO del if usuario:, que está DENTRO del elif opcion == "2":, que está DENTRO del while True:, que está DENTRO de la función. Cada nivel suma 4 espacios: la línea menu_admin(usuario) lleva 20 espacios. En Python, esa escalera reemplaza los FinSi y FinMientras de PSEINT. Si una línea queda en el nivel equivocado, el programa hace otra cosa sin avisar.menus/menu_principal.py y escribí los 5 importswhile True: con los print() del menú y el input()if/elif anidadosbreak en la opción 4 y el else de opción inválidaservices/usuario_service.py
Funciones ayudantes reutilizables: buscar, generar IDs, crear
registrar_usuario() hacía TODO: pedir datos, validar y guardar. Funciona, pero mezcla dos cosas: hablar con el usuario (inputs y prints) y la lógica pura (validar, generar id, guardar). Este archivo separa la lógica pura en funciones pequeñas y reutilizables. 🧩
crear_usuario() llama a buscar_usuario_por_correo() y a generar_id_usuario(), que están unas líneas más arriba. Dentro del mismo archivo no necesitas import: las llamas directo por su nombre. 🪆
from utils.file_manager import leer_json, guardar_json RUTA_USUARIOS = "data/usuarios.json" def obtener_usuarios(): """ Retorna la lista completa de usuarios registrados. """ # ① docstring: documentación oficial de la función return leer_json(RUTA_USUARIOS) def guardar_usuarios(usuarios): """ Guarda la lista completa de usuarios en el archivo JSON. """ guardar_json(RUTA_USUARIOS, usuarios) def generar_id_usuario(): """ Genera un ID automático para un nuevo usuario. """ usuarios = obtener_usuarios() if len(usuarios) == 0: return 1 # si no hay nadie, el primero es el 1 # ② busca el id MÁS ALTO y le suma 1 ultimo_id = max(usuario["id"] for usuario in usuarios) return ultimo_id + 1 def buscar_usuario_por_correo(correo): """ Busca un usuario por correo electrónico. Retorna el usuario si existe, de lo contrario retorna None. """ usuarios = obtener_usuarios() for usuario in usuarios: # ③ .lower() vuelve todo minúsculas antes de comparar if usuario["correo"].lower() == correo.lower(): return usuario return None def crear_usuario(nombre, correo, password, rol): """ Crea un nuevo usuario comprador o vendedor. """ usuarios = obtener_usuarios() # ④ reutilizamos la función de arriba — sin import, mismo archivo if buscar_usuario_por_correo(correo) is not None: return False, "Ya existe un usuario con ese correo." nuevo_usuario = { "id": generar_id_usuario(), "nombre": nombre, "correo": correo, "password": password, "rol": rol } usuarios.append(nuevo_usuario) guardar_usuarios(usuarios) # ⑤ ¡devolvemos DOS valores a la vez! return True, "Usuario registrado correctamente."
""" … """ — Un texto entre triples comillas justo debajo del def es la documentación oficial de la función. No es un comentario cualquiera: Spyder lo muestra cuando presionas Ctrl+I sobre el nombre de la función. En la documentación de tu proyecto final, los docstrings cuentan como documentación del código.max(usuario["id"] for usuario in usuarios) — Se lee: "de cada usuario de la lista, toma su id, y de todos esos ids dame el mayor". Es un for comprimido dentro de max(). ¿Por qué es mejor que len(usuarios) + 1 que usamos la clase pasada? Porque si eliminas al usuario 2 de 3, len()+1 daría 3… ¡un id que ya existe! Con max()+1 nunca se repite..lower() — Método de los strings que devuelve el texto en minúsculas. Al aplicarlo a AMBOS lados de la comparación, "Juan@Gmail.com" y "juan@gmail.com" se consideran el mismo correo. Detalle pequeño, experiencia de usuario grande.crear_usuario no repite el código de búsqueda: llama a buscar_usuario_por_correo(). Cada función hace UNA cosa y las demás la reutilizan. Cuando en tu proyecto notes que copias y pegas un bloque, detente: eso debe ser una función.return True, "mensaje" — retorno múltiple — Python permite devolver varios valores separados por coma (técnicamente, una tupla). Quien la llama los recibe así: estado, mensaje = crear_usuario(...). El primer valor dice SI funcionó, el segundo QUÉ pasó. Nota clave: esta función no tiene ni un print ni un input — es lógica pura, y quien imprime el mensaje es el menú. Esa separación es la que te pedirán en tu proyecto.listar_usuarios(), actualizar_usuario(id_usuario) y eliminar_usuario(id_usuario) — el UPDATE y DELETE del CRUD de usuarios. Los verás en acción con productos en el Paso 5 (el patrón es idéntico: buscar por id → si no existe avisar → modificar o eliminar → guardar). Escríbelos como reto en casa siguiendo ese patrón; el código de productos del siguiente paso es tu guía.services/usuario_service.py con el import y la constante de rutaobtener_usuarios() y guardar_usuarios() con sus docstringsgenerar_id_usuario() con max(...) y entiendo por qué es mejor que len()+1buscar_usuario_por_correo() con .lower()crear_usuario() y entiendo el return True, mensajeservices/producto_service.py
El CRUD completo: Create, Read, Update y Delete de productos
from utils.file_manager import leer_json, guardar_json RUTA_PRODUCTOS = "data/productos.json" def listar_productos(): productos = leer_json(RUTA_PRODUCTOS) print("\n===== PRODUCTOS DISPONIBLES =====") if len(productos) == 0: print("No hay productos publicados.") return # ① salida temprana: no hay nada que mostrar for producto in productos: if producto["stock"] > 0: # ② solo mostramos lo que hay en bodega print("--------------------------------") print(f"ID: {producto['id']}") print(f"Nombre: {producto['nombre']}") print(f"Categoría: {producto['categoria']}") print(f"Precio: ${producto['precio']}") print(f"Stock: {producto['stock']}") def crear_producto(usuario): # ③ recibe al vendedor logueado como parámetro productos = leer_json(RUTA_PRODUCTOS) print("\n===== PUBLICAR PRODUCTO =====") nombre = input("Nombre del producto: ") categoria = input("Categoría: ") try: # ④ input() da texto; lo convertimos a número precio = float(input("Precio: ")) stock = int(input("Stock: ")) except ValueError: print("Precio o stock inválido.") return if precio <= 0 or stock < 0: # ⑤ regla de negocio, no de formato print("El precio debe ser mayor a 0 y el stock no puede ser negativo.") return nuevo_producto = { "id": generar_id_producto(productos), "nombre": nombre, "categoria": categoria, "precio": precio, "stock": stock, "id_vendedor": usuario["id"] # ⑥ ¡aquí nace la relación entre archivos! } productos.append(nuevo_producto) guardar_json(RUTA_PRODUCTOS, productos) print("Producto publicado correctamente.") def generar_id_producto(productos): if len(productos) == 0: return 1 ultimo_id = max(producto["id"] for producto in productos) return ultimo_id + 1
return. Patrón que verás en TODO el proyecto: primero se descartan los casos donde no hay nada que hacer, y el "camino feliz" queda al final sin anidar tantos if.if producto["stock"] > 0: — Un if DENTRO del for: se evalúa producto por producto. Los agotados existen en el archivo pero no se muestran al comprador. También ve las f-strings: f"ID: {producto['id']}" incrusta el valor dentro del texto. Ojo al detalle: afuera comillas dobles, adentro comillas simples para la clave — así no chocan.def crear_producto(usuario): — ¿Recuerdas que menu_principal le pasó el usuario a menu_vendedor(usuario)? El menú se lo pasa ahora a esta función. El diccionario del vendedor viajó: auth_service → menu_principal → menu_vendedor → crear_producto. Así sabemos QUIÉN publica sin volver a preguntar.float(), int() y try/except ValueError — input() SIEMPRE devuelve texto: "85000" no es lo mismo que 85000. float() convierte a decimal e int() a entero. Pero si el usuario escribe "abc", la conversión lanza un error llamado ValueError. El try dice "intenta esto", y el except "si falla, haz esto otro en vez de morir". Sin él, el programa se cerraría de golpe frente a tu profe en el demo day. 😅try/except valida que sea un número (formato). Este if valida que el número tenga sentido (negocio): un precio de -500 es un número válido pero un dato absurdo. Tu proyecto necesita ambos tipos de validación."id_vendedor": usuario["id"] — El momento exacto donde se crea la relación que vimos en el Paso 1: guardamos el id del vendedor dentro del producto. Gracias a esto, más adelante podremos responder "¿cuáles son MIS productos?" y "¿cuánto ha vendido ESTE vendedor?".def listar_mis_productos(usuario): productos = leer_json(RUTA_PRODUCTOS) # ⑦ list comprehension: filtra la lista en 4 líneas mis_productos = [ producto for producto in productos if producto["id_vendedor"] == usuario["id"] ] print("\n===== MIS PRODUCTOS =====") if len(mis_productos) == 0: print("No tienes productos publicados.") return for producto in mis_productos: print("--------------------------------") print(f"ID: {producto['id']}") print(f"Nombre: {producto['nombre']}") print(f"Categoría: {producto['categoria']}") print(f"Precio: ${producto['precio']}") print(f"Stock: {producto['stock']}") def buscar_producto_por_nombre(): productos = leer_json(RUTA_PRODUCTOS) print("\n===== BUSCAR PRODUCTO =====") if len(productos) == 0: print("No hay productos registrados.") return nombre_busqueda = input("Ingrese el nombre del producto: ").lower() encontrados = [] # lista vacía que iremos llenando for producto in productos: # ⑧ "in" pregunta si un texto está CONTENIDO en otro if nombre_busqueda in producto["nombre"].lower(): encontrados.append(producto) if len(encontrados) == 0: print("No se encontraron productos.") return print("\n===== RESULTADOS =====") for producto in encontrados: print("--------------------------------") print(f"ID: {producto['id']}") print(f"Nombre: {producto['nombre']}") print(f"Categoría: {producto['categoria']}") print(f"Precio: ${producto['precio']}") print(f"Stock: {producto['stock']}")
[producto for producto in productos if producto["id_vendedor"] == usuario["id"]] se lee: "dame una lista nueva con cada producto de la lista original, PERO solo los que cumplan la condición". Es el equivalente compacto de crear una lista vacía + for + if + append. Compárala con buscar_producto_por_nombre justo debajo, que hace lo mismo en la versión larga: ambas son válidas, la comprehension es la forma "pythonica".in para buscar dentro de texto — "audi" in "audífonos bluetooth" es True. Por eso el usuario no necesita escribir el nombre completo: con un pedazo basta. Combinado con .lower() en ambos lados, la búsqueda ignora mayúsculas. Este mismo in también sirve para listas: 3 in [1, 2, 3] es True — lo usaremos en ventas.def actualizar_producto(usuario): productos = leer_json(RUTA_PRODUCTOS) mis_productos = [ producto for producto in productos if producto["id_vendedor"] == usuario["id"] ] print("\n===== ACTUALIZAR PRODUCTO =====") if len(mis_productos) == 0: print("No tienes productos para actualizar.") return listar_mis_productos(usuario) # reutilizamos: mostrar antes de elegir try: id_producto = int(input("\nIngrese el ID del producto a actualizar: ")) except ValueError: print("Debe ingresar un ID válido.") return # ⑨ patrón "buscar y quedarse con la referencia" producto_encontrado = None for producto in productos: if producto["id"] == id_producto and producto["id_vendedor"] == usuario["id"]: producto_encontrado = producto break # ya lo encontramos: no hay que seguir buscando if producto_encontrado is None: print("No se encontró un producto suyo con ese ID.") return print("\nDeje el campo vacío si no desea modificarlo.") # ⑩ mostramos el valor actual entre paréntesis como pista nuevo_nombre = input(f"Nuevo nombre ({producto_encontrado['nombre']}): ") nueva_categoria = input(f"Nueva categoría ({producto_encontrado['categoria']}): ") nuevo_precio = input(f"Nuevo precio ({producto_encontrado['precio']}): ") nuevo_stock = input(f"Nuevo stock ({producto_encontrado['stock']}): ") if nuevo_nombre != "": # ⑪ vacío = "no quiero cambiarlo" producto_encontrado["nombre"] = nuevo_nombre if nueva_categoria != "": producto_encontrado["categoria"] = nueva_categoria if nuevo_precio != "": try: nuevo_precio = float(nuevo_precio) if nuevo_precio <= 0: print("El precio debe ser mayor a 0.") return producto_encontrado["precio"] = nuevo_precio except ValueError: print("Precio inválido.") return if nuevo_stock != "": try: nuevo_stock = int(nuevo_stock) if nuevo_stock < 0: print("El stock no puede ser negativo.") return producto_encontrado["stock"] = nuevo_stock except ValueError: print("Stock inválido.") return guardar_json(RUTA_PRODUCTOS, productos) # ⑫ guardamos la lista COMPLETA print("Producto actualizado correctamente.") def eliminar_producto(usuario): productos = leer_json(RUTA_PRODUCTOS) mis_productos = [ producto for producto in productos if producto["id_vendedor"] == usuario["id"] ] print("\n===== ELIMINAR PRODUCTO =====") if len(mis_productos) == 0: print("No tienes productos para eliminar.") return listar_mis_productos(usuario) try: id_producto = int(input("\nIngrese el ID del producto a eliminar: ")) except ValueError: print("Debe ingresar un ID válido.") return producto_encontrado = None for producto in productos: if producto["id"] == id_producto and producto["id_vendedor"] == usuario["id"]: producto_encontrado = producto break if producto_encontrado is None: print("No se encontró un producto suyo con ese ID.") return # ⑬ pedir confirmación antes de una acción destructiva confirmacion = input( f"¿Está seguro de eliminar el producto '{producto_encontrado['nombre']}'? (s/n): " ).lower() if confirmacion == "s": productos.remove(producto_encontrado) # ⑭ saca el elemento de la lista guardar_json(RUTA_PRODUCTOS, productos) print("Producto eliminado correctamente.") else: print("Eliminación cancelada.")
producto_encontrado = None (aún no hay nada), recorremos, y al encontrar coincidencia la guardamos y cortamos con break. Si el for termina y la variable sigue en None, no existía. Nota la doble condición con and: el id debe coincidir Y el producto debe ser TUYO — un vendedor no puede editar productos ajenos. Eso es seguridad por rol, y tu proyecto la necesita.Nuevo precio (85000.0):) le recuerda al usuario qué hay guardado. Detalles así suman puntos en el demo day.input() devuelve "" (cadena vacía) y el if se salta la asignación: el valor viejo sobrevive. Así puedes cambiar SOLO el precio sin re-escribir todo.productos y no producto_encontrado? — Truco importante de Python: producto_encontrado NO es una copia, es una referencia al mismo diccionario que vive dentro de la lista productos. Al modificarlo, la lista ya quedó modificada en memoria. Solo falta escribir la lista completa al disco..lower() al final del input acepta tanto "S" como "s".lista.remove(elemento) — Busca el elemento dentro de la lista y lo saca. Compara: append() agrega al final, remove() quita. De nuevo, después de modificar en memoria hay que persistir con guardar_json().actualizar y eliminar comparten el mismo esqueleto: leer → filtrar los míos → mostrar → pedir id → buscar → validar que existe → actuar → guardar. Memorízalo: es la receta de cualquier UPDATE/DELETE de tu proyecto. Cambia "producto" por "cliente" (CRM), "curso" (LMS) o "petición" (PQRS) y el código es casi el mismo.services/producto_service.py con import y constante RUTA_PRODUCTOSlistar_productos() con el filtro de stock > 0crear_producto(usuario) con try/except ValueError y validacionesgenerar_id_producto(productos) con max()listar_mis_productos(usuario) y entiendo la list comprehensionactualizar_producto(usuario) con el patrón buscar → validar → modificar → guardareliminar_producto(usuario) con confirmación (s/n) y remove()buscar_producto_por_nombre() con in y .lower()menus/menu_vendedor.py
La pantalla del vendedor: 5 opciones que llaman al servicio
while True que muestra opciones y delega TODO al servicio. Fíjate que el menú no tiene lógica de negocio: no valida precios, no toca JSON. Solo dibuja y enruta. 🎨
# ① import multilínea: paréntesis para no hacer una línea kilométrica from services.producto_service import ( crear_producto, listar_mis_productos, actualizar_producto, eliminar_producto ) def menu_vendedor(usuario): # ② recibe el dict que envió menu_principal while True: print("\n========== MENÚ VENDEDOR ==========") print(f"Bienvenido vendedor: {usuario['nombre']}") print("1. Publicar producto") print("2. Ver mis productos") print("3. Actualizar producto") print("4. Eliminar producto") print("5. Cerrar sesión") opcion = input("Seleccione una opción: ") if opcion == "1": crear_producto(usuario) # ③ el usuario sigue viajando hacia abajo elif opcion == "2": listar_mis_productos(usuario) elif opcion == "3": actualizar_producto(usuario) elif opcion == "4": eliminar_producto(usuario) elif opcion == "5": print("Sesión cerrada correctamente.") break # ④ vuelve al menú principal, NO cierra el programa else: print("Opción no válida. Intente nuevamente.")
\. Cada función va en su renglón con coma. Más legible y más fácil de modificar.menu_vendedor(usuario) — El parámetro llega desde menu_principal, que lo recibió de iniciar_sesion(). Por eso podemos saludar por nombre sin volver a preguntar quién eres.break ≠ cerrar el programa — Este break solo rompe el while de ESTE menú. La función termina y el control regresa a quien la llamó: menu_principal, cuyo propio while True sigue vivo y vuelve a mostrar sus opciones. Los menús se apilan como cajas: al cerrar la de adentro, sigues dentro de la de afuera.menus/menu_vendedor.py con el import multilínea entre paréntesiswhile True con las 5 opciones y el saludo con f-stringdata/productos.json y vi mi producto guardado con su id_vendedorservices/venta_service.py
Carrito en memoria, compra que descuenta stock, historiales
menu_comprador (lo verás en el Paso 8) y se pasa como argumento a estas funciones. Cuando cierras sesión, la lista muere y el carrito se vacía. Por eso, si sales sin comprar, pierdes el carrito — igual que en la vida real cuando abandonas el carrito en el supermercado. 😄
from utils.file_manager import leer_json, guardar_json from datetime import datetime # ① librería estándar para fechas y horas RUTA_PRODUCTOS = "data/productos.json" RUTA_VENTAS = "data/ventas.json" # este servicio usa DOS archivos def agregar_al_carrito(carrito): # ② recibe la lista que vive en el menú productos = leer_json(RUTA_PRODUCTOS) if len(productos) == 0: print("No hay productos disponibles.") return print("\n===== AGREGAR PRODUCTO AL CARRITO =====") try: id_producto = int(input("Ingrese el ID del producto: ")) cantidad = int(input("Ingrese la cantidad: ")) except ValueError: print("Debe ingresar valores numéricos.") return producto_encontrado = None # el patrón de búsqueda que ya conoces for producto in productos: if producto["id"] == id_producto: producto_encontrado = producto break if producto_encontrado is None: print("Producto no encontrado.") return if cantidad <= 0: print("La cantidad debe ser mayor a cero.") return if cantidad > producto_encontrado["stock"]: # ③ no vender lo que no hay print("No hay suficiente stock disponible.") return # ④ el item del carrito es un diccionario NUEVO, hecho a la medida item_carrito = { "id_producto": producto_encontrado["id"], "nombre": producto_encontrado["nombre"], "cantidad": cantidad, "precio_unitario": producto_encontrado["precio"], "subtotal": cantidad * producto_encontrado["precio"] } carrito.append(item_carrito) # ⑤ modificamos la lista del menú ¡sin return! print("Producto agregado al carrito correctamente.") def ver_carrito(carrito): print("\n===== CARRITO DE COMPRAS =====") if len(carrito) == 0: print("El carrito está vacío.") return total = 0 # ⑥ acumulador: arranca en 0 y crece for item in carrito: print("--------------------------------") print(f"Producto: {item['nombre']}") print(f"Cantidad: {item['cantidad']}") print(f"Precio unitario: ${item['precio_unitario']}") print(f"Subtotal: ${item['subtotal']}") total += item["subtotal"] # total = total + subtotal print("--------------------------------") print(f"TOTAL A PAGAR: ${total}")
from datetime import datetime — Librería estándar (no se instala). Sí, el nombre se repite: el módulo datetime contiene una clase llamada datetime. La usaremos para sellar cada venta con fecha y hora exactas.agregar_al_carrito(carrito) — La lista carrito NO se crea aquí: nace en menu_comprador y llega como argumento. En Python, las listas se pasan por referencia: la función recibe la lista original, no una copia. Todo lo que le haga aquí, el menú lo verá.precio_unitario al carrito en vez de solo el id. ¿Por qué? Si el vendedor sube el precio 5 minutos después, tu carrito conserva el precio que viste. Y calculamos subtotal de una vez: cantidad * precio.return — Esta función no devuelve nada: su efecto es agregar el item a la lista compartida. Como el menú tiene la MISMA lista (referencia, no copia), el cambio persiste entre llamadas. Así el carrito "recuerda" entre una opción del menú y la siguiente.total = 0 antes del ciclo, total += item["subtotal"] dentro. += es el atajo de total = total + …. Al salir del for, total tiene la suma completa.def realizar_compra(usuario, carrito): # recibe DOS argumentos if len(carrito) == 0: print("No puede realizar una compra con el carrito vacío.") return productos = leer_json(RUTA_PRODUCTOS) ventas = leer_json(RUTA_VENTAS) # ⑦ Validación por si ventas.json está como {} o vacío mal estructurado if not isinstance(ventas, list): ventas = [] total = 0 for item in carrito: total += item["subtotal"] nueva_venta = { "id": len(ventas) + 1, "id_comprador": usuario["id"], "nombre_comprador": usuario["nombre"], "productos": carrito.copy(), # ⑧ una COPIA, no la lista original "total": total, "fecha": datetime.now().strftime("%Y-%m-%d %H:%M:%S") # ⑨ } # ⑩ for anidado: por cada item del carrito, buscar su producto y descontar for item in carrito: for producto in productos: if producto["id"] == item["id_producto"]: producto["stock"] -= item["cantidad"] ventas.append(nueva_venta) guardar_json(RUTA_VENTAS, ventas) # ⑪ persistimos LOS DOS archivos guardar_json(RUTA_PRODUCTOS, productos) carrito.clear() # ⑫ vaciar el carrito tras comprar print("\nCompra realizada correctamente.") print(f"Total pagado: ${total}") def ver_historial_compras(usuario): ventas = leer_json(RUTA_VENTAS) compras_usuario = [ # filtrar MIS compras: comprehension otra vez venta for venta in ventas if venta["id_comprador"] == usuario["id"] ] print("\n===== HISTORIAL DE COMPRAS =====") if len(compras_usuario) == 0: print("No tienes compras registradas.") return for venta in compras_usuario: print("--------------------------------") print(f"ID venta: {venta['id']}") print(f"Fecha: {venta['fecha']}") print(f"Total: ${venta['total']}") print("Productos:") for producto in venta["productos"]: # ⑬ for dentro del for: lista anidada print( f"- {producto['nombre']} | " f"Cantidad: {producto['cantidad']} | " f"Subtotal: ${producto['subtotal']}" )
isinstance(ventas, list) — Pregunta si la variable es del tipo lista. Programación defensiva: si alguien dejó ventas.json con {} en vez de [], el append de abajo fallaría. Mejor detectarlo y corregir a [].carrito.copy() — el detalle más fino del proyecto — Si guardáramos carrito directo, la venta apuntaría a LA MISMA lista… que dos líneas después vaciamos con clear(). ¡La venta quedaría sin productos! copy() crea una lista nueva independiente. Recuerda: asignar una lista NO la copia, solo crea otro nombre para la misma caja.datetime.now().strftime("%Y-%m-%d %H:%M:%S") — now() da la fecha/hora actual; strftime ("string format time") la convierte a texto con el formato que pidas: %Y año de 4 cifras, %m mes, %d día, %H:%M:%S hora:minuto:segundo. Resultado: "2026-07-02 10:30:00". Un JSON no puede guardar objetos fecha, solo texto — por eso la conversión.for anidado que descuenta stock — Por cada item comprado (for externo) recorremos todos los productos (for interno) hasta hallar el suyo, y restamos: producto["stock"] -= item["cantidad"]. Como producto es una referencia al diccionario dentro de productos, la lista completa queda actualizada en memoria.carrito.clear() — Vacía la lista original (la del menú, por referencia). El comprador puede iniciar otra compra con carrito limpio, sin cerrar sesión."productos" adentro (¿recuerdas el Paso 1?). Un for dentro de otro for: el externo recorre ventas, el interno los productos de ESA venta. Y mira el print de varias líneas: varias f-strings seguidas entre paréntesis se unen solas en un solo texto.ver_ventas_vendedor(usuario): filtra los productos del vendedor, junta sus ids en una lista, y recorre las ventas preguntando con if item["id_producto"] in ids_productos_vendedor: cuáles items le pertenecen, acumulando total_vendido. Usa solo herramientas que ya conoces: comprehensions, in sobre listas y el acumulador. Escríbela como reto guiándote del código completo del repositorio del curso.services/venta_service.py con los imports (incluido datetime) y las DOS rutasagregar_al_carrito(carrito) con sus 3 validaciones en cascadaver_carrito(carrito) con el acumulador de totalrealizar_compra(usuario, carrito) y entiendo por qué carrito.copy()ver_historial_compras(usuario) con el for anidadomenus/menu_comprador.py
Aquí nace el carrito y se comparte con todas las opciones
carrito = []. Ese es el momento exacto en que nace el carrito: una lista vacía creada al entrar al menú, ANTES del while. Todas las opciones comparten esa misma lista. 🛍️
from services.producto_service import listar_productos, buscar_producto_por_nombre from services.venta_service import agregar_al_carrito, ver_carrito, realizar_compra, ver_historial_compras def menu_comprador(usuario): carrito = [] # ① nace ANTES del while → sobrevive cada vuelta while True: print("\n========== MENÚ COMPRADOR ==========") print("1. Ver productos disponibles") print("2. Buscar producto por nombre") print("3. Agregar producto al carrito") print("4. Ver carrito") print("5. Realizar compra") print("6. Ver historial de compras") print("7. Cerrar sesión") opcion = input("Seleccione una opción: ") if opcion == "1": listar_productos() elif opcion == "2": buscar_producto_por_nombre() elif opcion == "3": agregar_al_carrito(carrito) # ② la MISMA lista viaja a cada función elif opcion == "4": ver_carrito(carrito) elif opcion == "5": realizar_compra(usuario, carrito) # ③ dos argumentos: quién y qué elif opcion == "6": ver_historial_compras(usuario) elif opcion == "7": print("Sesión cerrada correctamente.") break # ④ al salir, el carrito muere con la función else: print("Opción no válida. Intente nuevamente.")
carrito = [] antes del while — La posición lo es todo. Si estuviera DENTRO del while, cada vuelta del menú crearía un carrito nuevo vacío y perderías lo agregado. Al estar antes, la lista sobrevive todas las vueltas mientras no salgas de la función. Es una variable local de menu_comprador: nadie más la ve, solo las funciones a las que se la pasamos.realizar_compra(usuario, carrito) — El orden de los argumentos importa y debe coincidir con el def realizar_compra(usuario, carrito): del servicio. Primero quién compra, luego qué compra.break (logout). Si vuelves a iniciar sesión, arranca vacío otra vez. Los datos permanentes van a JSON; los temporales, a variables. Decidir cuál es cuál será una de las preguntas clave de TU proyecto.menus/menu_comprador.py con los imports de los 2 servicioscarrito = [] ANTES del while True y sé explicar por quécarrito y usuariodata/productos.json y confirmé que el stock bajó tras la compraservices/admin_service.py y menus/menu_admin.py
El rol con visión total: supervisar usuarios, productos y ventas
usuarios.json a mano y poniendo "rol": "admin". Es una decisión de diseño: nadie puede auto-nombrarse administrador desde la aplicación. En tu proyecto harán lo mismo con el rol de coordinador/administrador. 🔐
from utils.file_manager import leer_json, guardar_json RUTA_USUARIOS = "data/usuarios.json" # ① el admin usa LOS TRES archivos RUTA_PRODUCTOS = "data/productos.json" RUTA_VENTAS = "data/ventas.json" def ver_usuarios(): usuarios = leer_json(RUTA_USUARIOS) print("\n===== USUARIOS REGISTRADOS =====") if len(usuarios) == 0: print("No hay usuarios registrados.") return for usuario in usuarios: # ② SIN filtro: los ve todos print("--------------------------------") print(f"ID: {usuario['id']}") print(f"Nombre: {usuario['nombre']}") print(f"Correo: {usuario['correo']}") print(f"Rol: {usuario['rol']}") # nota: NUNCA imprimimos el password def ver_ventas_generales(): ventas = leer_json(RUTA_VENTAS) print("\n===== VENTAS GENERALES =====") if len(ventas) == 0: print("No hay ventas registradas.") return total_general = 0 # ③ acumulador sobre TODAS las ventas for venta in ventas: print("--------------------------------") print(f"ID venta: {venta['id']}") print(f"Comprador: {venta['nombre_comprador']}") print(f"Fecha: {venta['fecha']}") print(f"Total venta: ${venta['total']}") for producto in venta["productos"]: print(f"- {producto['nombre']} | Cantidad: {producto['cantidad']}") total_general += venta["total"] print("--------------------------------") print(f"TOTAL GENERAL DE VENTAS: ${total_general}")
producto_service (una ruta) y venta_service (dos): cada servicio conoce SOLO los archivos que necesita.listar_mis_productos filtraba con if producto["id_vendedor"] == usuario["id"]. El admin recorre sin if: lo ve todo. La diferencia entre roles no es magia, son filtros. Detalle de seguridad: mostramos el rol pero JAMÁS el password por pantalla.ver_carrito, pero sumando el total de TODAS las ventas del sistema. Los "reportes" que pide tu proyecto final son exactamente esto: recorrer, filtrar y acumular.ver_productos() (igual que ver_usuarios pero con productos, mostrando también id_vendedor) y eliminar_producto_admin() (el mismo patrón eliminar del Paso 5, pero SIN la condición de dueño: el admin puede eliminar cualquier producto). Luego crea menus/menu_admin.py: es un calco de menu_vendedor.py con 5 opciones que llaman a ver_usuarios(), ver_productos(), eliminar_producto_admin(), ver_ventas_generales() y el break para cerrar sesión. ¡Ya tienes todas las piezas para escribirlo solo!services/admin_service.py con las 3 rutas y ver_usuarios()ver_productos(), eliminar_producto_admin() y ver_ventas_generales()menus/menu_admin.py siguiendo el patrón de menu_vendedor.pyusuarios.json a mano para crear mi usuario con "rol": "admin"main.py (F5) y probé los 3 roles de punta a punta 🎉| En PyMarket | CRM | Mini LMS | Sistema PQRS |
|---|---|---|---|
| usuarios.json + roles | Asesores, gerente, clientes | Estudiantes, docentes, coordinador | Ciudadanos, funcionarios, supervisor |
| productos.json | Clientes / oportunidades | Cursos y actividades | Peticiones, quejas, reclamos |
| ventas.json | Interacciones / ventas cerradas | Entregas y calificaciones | Respuestas y cierres de caso |
| id_vendedor (relación) | id_asesor en cada cliente | id_docente en cada curso | id_funcionario en cada caso |
| carrito (dato temporal) | Cotización en armado | Intento de quiz en curso | Borrador de la petición |
| ver_ventas_generales() | Reporte de ventas del mes | Reporte de notas del curso | Casos resueltos vs pendientes |
menus/, services/, utils/, data/ y copien su file_manager.py — esa utilidad es igual en los 3 proyectos. ④ Repartan los servicios entre los integrantes según sus roles: como cada servicio es un archivo independiente, pueden programar en paralelo sin pisarse.try/except!)README.md (qué hace el sistema, cómo ejecutarlo con main.py, estructura de carpetas, roles e integrantes) + un documento libre con lo que decidieron y por quétry/except ValueError y el return [] del file_manager. Practiquen provocar esos errores a propósito en el ensayo.
Hoy completaste PyMarket de punta a punta. Entendiste la capa de datos: tres JSON que son listas de diccionarios relacionadas por ids (como llaves foráneas). Construiste main.py con su if __name__ == "__main__": y el menú principal que enruta por rol. En usuario_service viste funciones puras que se reutilizan entre sí y el retorno múltiple return True, mensaje. En producto_service dominaste el CRUD completo con el patrón leer → buscar → validar → actuar → guardar, list comprehensions, try/except ValueError y la seguridad por dueño. En venta_service aprendiste que el carrito vive en RAM y viaja por referencia, que carrito.copy() evita perder la venta, y que una compra es una transacción que guarda dos archivos. Y con el admin viste que los roles son, al final, filtros.
Lo más importante: ya no estás copiando código, estás reconociendo patrones. Tu CRM, mini LMS o PQRS usa exactamente estas piezas con otros nombres. Definan sus JSON, dibujen su diagrama, repartan los servicios — y a construir. Nos vemos en el demo day. 🚀