Saltar a contenido

Proyecto corto 1 · La caja registradora de TechShop

Objetivos. En este proyecto escribirás y probarás tu primer programa completo: un menú de terminal que muestra un catálogo, permite añadir productos a un carrito, calcula el importe con un posible descuento y genera un ticket formateado. Solo necesitarás lo aprendido en 1.2–1.7.

Evidencia de logro. Entregarás el script 05b_caja_registradora.py, junto con una ejecución de ejemplo y una breve explicación de la herramienta utilizada para resolver cada requisito.

Contexto y motivación

En los capítulos anteriores has aprendido a usar variables, condicionales, bucles, listas y diccionarios. Ahora toca combinarlos para construir un programa que funcione de principio a fin.

TechShop va a abrir su primer punto de venta físico y necesita una caja registradora de terminal. Para este proyecto no habrá base de datos ni librerías externas: solo Python y las herramientas que ya conoces. Está pensado para unas 3 horas y sirve para consolidar lo aprendido en la semana 4; no es un examen.

El proyecto también te permitirá detectar código que se repite. Esa observación prepara el siguiente capítulo, 1.8 · Funciones: allí aprenderás a escribir una operación una sola vez y reutilizarla. Conserva tu solución, porque volverás a trabajar con ella.

Vocabulario

Término Significado en el proyecto
Catálogo Los productos disponibles, con su precio
Carrito Lo que el cliente va comprando: {código: unidades}
Subtotal Precio × unidades de una línea
Total Suma de los subtotales
Ticket El resumen final de la compra
Menú El bucle que muestra opciones y ejecuta la elegida
Acumulador Variable que va sumando resultados (total += subtotal)

Prerrequisitos

El programa no utiliza funciones (se introducen en 1.8), ficheros (1.10) ni librerías externas (1.15).

El enunciado

La caja debe cumplir estos requisitos:

  1. Mostrar un menú con 5 opciones y repetirse hasta que se elija salir.
  2. Ver catálogo: listar los productos con su precio alineado en columnas.
  3. Añadir al carrito: pedir código y unidades; avisar si el código no existe o las unidades son menores que 1.
  4. Ver carrito: listar las líneas, el total y el número de unidades.
  5. Cobrar: emitir un ticket; si el total llega a 60 €, aplicar un descuento del 10 %; vaciar el carrito tras cobrar.
  6. Salir con un mensaje de despedida.

1. Alinear los precios

Para que el catálogo y el ticket sean fáciles de leer, los precios deben quedar alineados en columnas. Ya conoces las f-strings de 1.2; en este ejemplo se introduce una opción que estudiarás con más detalle en 1.9 · Cadenas: dentro de las llaves puedes indicar un ancho fijo y el número de decimales. :>8.2f alinea a la derecha en un campo de 8 caracteres y muestra 2 decimales; :<8 alinea a la izquierda:

precio = 14.5
print(f"  {precio:>8.2f} €")
print(f"  {'Total':<8}{66.42:>8.2f} €")

Salida esperada:

     14.50 €
  Total      66.42 €

Al usar un ancho fijo, los precios ocupan el mismo espacio aunque tengan distintas longitudes y las columnas quedan alineadas. Cuando estudies 1.9, puedes volver a este ejemplo para consultar las demás opciones de formato.

El catálogo es un diccionario de diccionarios: cada código apunta a otro diccionario con los datos del producto. Los códigos son enteros, no texto.

catalogo = {
    101: {"nombre": "Camiseta TechShop", "precio": 14.50},
    102: {"nombre": "Taza de la marca", "precio": 8.90},
    103: {"nombre": "Sudadera con capucha", "precio": 32.00},
    104: {"nombre": "Pegatinas (pack x5)", "precio": 3.20},
    105: {"nombre": "Gorra bordada", "precio": 18.00},
}

print("CATÁLOGO")
for codigo, producto in catalogo.items():
    print(f"  {codigo} · {producto['nombre']:<24}{producto['precio']:>7.2f} €")

Salida esperada:

CATÁLOGO
  101 · Camiseta TechShop         14.50 €
  102 · Taza de la marca           8.90 €
  103 · Sudadera con capucha      32.00 €
  104 · Pegatinas (pack x5)        3.20 €
  105 · Gorra bordada             18.00 €

Esta estructura resulta útil porque el código es la clave: comprobar si un producto existe (codigo in catalogo) es inmediato y no hace falta recorrer una lista.

3. Paso 2: añadir al carrito

El carrito relaciona cada código con sus unidades: {código: unidades}. Lo importante es acumular las nuevas unidades cuando el producto ya está en el carrito. Para ello usaremos .get(clave, valor_si_no_está):

carrito = {}
carrito[101] = carrito.get(101, 0) + 2   # primeras 2 camisetas
carrito[103] = carrito.get(103, 0) + 1
carrito[104] = carrito.get(104, 0) + 4
print(carrito)

Salida esperada:

{101: 2, 103: 1, 104: 4}

Si el cliente vuelve a pedir camisetas, carrito.get(101, 0) devuelve 2 y las nuevas unidades se suman a las que ya había. Una asignación como carrito[101] = 2 sobrescribiría el valor anterior. Es uno de los errores que conviene evitar.

4. Paso 3: ver el carrito y el total

Para mostrar el carrito, lo recorremos con .items() y mantenemos dos acumuladores: uno para las unidades y otro para el total.

print("CARRITO")
total_unidades = 0
total = 0.0
for codigo, unidades in carrito.items():
    nombre = catalogo[codigo]["nombre"]
    subtotal = catalogo[codigo]["precio"] * unidades
    total_unidades += unidades
    total += subtotal
    print(f"  {unidades:>2} × {nombre:<24}{subtotal:>7.2f} €")
print(f"  {'Total':<28}{total:>7.2f} € ({total_unidades} unidades)")

Salida esperada:

CARRITO
   2 × Camiseta TechShop         29.00 €
   1 × Sudadera con capucha      32.00 €
   4 × Pegatinas (pack x5)       12.80 €
  Total                         73.80 € (7 unidades)

El carrito solo guarda códigos, no nombres ni precios: estos datos se consultan en el catálogo cuando hacen falta. Así, cada dato se guarda una sola vez.

5. Paso 4: el descuento

El descuento se decide con un if sobre el total acumulado. El umbral y el porcentaje se guardan en constantes con nombre, al principio del programa, para poder cambiarlos desde un único lugar:

DESCUENTO_MINIMO = 60.0   # total a partir del que se aplica el descuento
DESCUENTO = 0.10          # 10 %

# El total del paso 3, que aquí fijamos para poder ejecutar el bloque suelto.
total = 73.80

if total >= DESCUENTO_MINIMO:
    ahorro = total * DESCUENTO
    total = total - ahorro
    print(f"Descuento 10 %: -{ahorro:.2f} €")
print(f"Total final: {total:.2f} €")

Salida esperada:

Descuento 10 %: -7.38 €
Total final: 66.42 €

El {ahorro:.2f} de la f-string significa «muestra el número con dos decimales». Es un formato, no un redondeo del valor: si ahorro fuera 7.3456, imprimiría -7.35 €.

Cambia la línea total = 73.80 por total = 45.00 y vuelve a ejecutar: la condición no se cumple, no se imprime la línea del descuento y el total final queda intacto. Esa es la comprobación de que el if está haciendo su trabajo.

6. El programa completo

Ahora reunimos las piezas en un while True. Usaremos if/elif según la opción elegida, break para salir y continue para volver al menú después de mostrar un aviso. Guarda el programa como 05b_caja_registradora.py y ejecútalo en la terminal con uv run python 05b_caja_registradora.py:

# 05b_caja_registradora.py — Proyecto corto 1 · La caja registradora de TechShop

catalogo = {
    101: {"nombre": "Camiseta TechShop", "precio": 14.50},
    102: {"nombre": "Taza de la marca", "precio": 8.90},
    103: {"nombre": "Sudadera con capucha", "precio": 32.00},
    104: {"nombre": "Pegatinas (pack x5)", "precio": 3.20},
    105: {"nombre": "Gorra bordada", "precio": 18.00},
}

carrito = {}                 # {código: unidades}
DESCUENTO_MINIMO = 60.0      # total a partir del que se aplica el descuento
DESCUENTO = 0.10             # 10 %

while True:
    print()
    print("=== CAJA REGISTRADORA TECHSHOP ===")
    print(" 1) Ver catálogo")
    print(" 2) Añadir al carrito")
    print(" 3) Ver carrito")
    print(" 4) Cobrar y emitir ticket")
    print(" 0) Salir")
    opcion = input("Elige una opción: ")

    if opcion == "1":
        print("CATÁLOGO")
        for codigo, producto in catalogo.items():
            print(f"  {codigo} · {producto['nombre']:<24}{producto['precio']:>7.2f} €")

    elif opcion == "2":
        codigo = int(input("Código del producto: "))
        if codigo not in catalogo:
            print("  Aviso: ese código no existe en el catálogo.")
            continue
        unidades = int(input("¿Cuántas unidades? "))
        if unidades < 1:
            print("  Aviso: las unidades deben ser al menos 1.")
            continue
        carrito[codigo] = carrito.get(codigo, 0) + unidades
        print(f"  Añadidas {unidades} × {catalogo[codigo]['nombre']}.")

    elif opcion == "3":
        if len(carrito) == 0:
            print("  El carrito está vacío.")
            continue
        print("CARRITO")
        total_unidades = 0
        total = 0.0
        for codigo, unidades in carrito.items():
            nombre = catalogo[codigo]["nombre"]
            subtotal = catalogo[codigo]["precio"] * unidades
            total_unidades += unidades
            total += subtotal
            print(f"  {unidades:>2} × {nombre:<24}{subtotal:>7.2f} €")
        print(f"  {'Total':<28}{total:>7.2f} € ({total_unidades} unidades)")

    elif opcion == "4":
        if len(carrito) == 0:
            print("  Aviso: no puedes cobrar con el carrito vacío.")
            continue
        print("=" * 38)
        print(f"{'TICKET TECHSHOP':^38}")
        print("=" * 38)
        total = 0.0
        for codigo, unidades in carrito.items():
            nombre = catalogo[codigo]["nombre"]
            subtotal = catalogo[codigo]["precio"] * unidades
            total += subtotal
            print(f"{unidades:>2} × {nombre:<24}{subtotal:>7.2f} €")
        print("-" * 38)
        print(f"{'Base':<29}{total:>7.2f} €")
        if total >= DESCUENTO_MINIMO:
            ahorro = total * DESCUENTO
            total = total - ahorro
            print(f"{'Descuento 10 %':<29}{-ahorro:>7.2f} €")
        print(f"{'TOTAL A PAGAR':<29}{total:>7.2f} €")
        print("=" * 38)
        print("¡Gracias por tu compra!")
        carrito = {}

    elif opcion == "0":
        print("Hasta pronto.")
        break

    else:
        print("  Aviso: opción no válida.")

Esta es una sesión completa de ejemplo. En la transcripción, el texto que aparece después de : representa lo que escribe el usuario:

=== CAJA REGISTRADORA TECHSHOP ===
 1) Ver catálogo
 2) Añadir al carrito
 3) Ver carrito
 4) Cobrar y emitir ticket
 0) Salir
Elige una opción: 1
CATÁLOGO
  101 · Camiseta TechShop         14.50 €
  102 · Taza de la marca           8.90 €
  103 · Sudadera con capucha      32.00 €
  104 · Pegatinas (pack x5)        3.20 €
  105 · Gorra bordada             18.00 €

=== CAJA REGISTRADORA TECHSHOP ===
 1) Ver catálogo
 2) Añadir al carrito
 3) Ver carrito
 4) Cobrar y emitir ticket
 0) Salir
Elige una opción: 2
Código del producto: 101
¿Cuántas unidades? 2
  Añadidas 2 × Camiseta TechShop.

=== CAJA REGISTRADORA TECHSHOP ===
 1) Ver catálogo
 2) Añadir al carrito
 3) Ver carrito
 4) Cobrar y emitir ticket
 0) Salir
Elige una opción: 2
Código del producto: 103
¿Cuántas unidades? 1
  Añadidas 1 × Sudadera con capucha.

=== CAJA REGISTRADORA TECHSHOP ===
 1) Ver catálogo
 2) Añadir al carrito
 3) Ver carrito
 4) Cobrar y emitir ticket
 0) Salir
Elige una opción: 2
Código del producto: 104
¿Cuántas unidades? 4
  Añadidas 4 × Pegatinas (pack x5).

=== CAJA REGISTRADORA TECHSHOP ===
 1) Ver catálogo
 2) Añadir al carrito
 3) Ver carrito
 4) Cobrar y emitir ticket
 0) Salir
Elige una opción: 3
CARRITO
   2 × Camiseta TechShop         29.00 €
   1 × Sudadera con capucha      32.00 €
   4 × Pegatinas (pack x5)       12.80 €
  Total                         73.80 € (7 unidades)

=== CAJA REGISTRADORA TECHSHOP ===
 1) Ver catálogo
 2) Añadir al carrito
 3) Ver carrito
 4) Cobrar y emitir ticket
 0) Salir
Elige una opción: 4
======================================
           TICKET TECHSHOP
======================================
 2 × Camiseta TechShop         29.00 €
 1 × Sudadera con capucha      32.00 €
 4 × Pegatinas (pack x5)       12.80 €
--------------------------------------
Base                           73.80 €
Descuento 10 %                 -7.38 €
TOTAL A PAGAR                  66.42 €
======================================
¡Gracias por tu compra!

=== CAJA REGISTRADORA TECHSHOP ===
 1) Ver catálogo
 2) Añadir al carrito
 3) Ver carrito
 4) Cobrar y emitir ticket
 0) Salir
Elige una opción: 0
Hasta pronto.

Práctica de transferencia

Prueba estos casos: un código inexistente (999), cero unidades, intentar cobrar con el carrito vacío y una opción que no existe (7). Para cada caso, anota la entrada, el aviso que aparece y el estado del carrito después de la operación. El programa debe volver al menú y no aceptar el dato incorrecto.

7. Qué nos falta para reutilizar el código

Si revisas el programa, encontrarás dos repeticiones importantes:

  • El bucle que recorre el carrito y suma el total aparece dos veces (en las opciones 3 y 4). Si mañana añades el IVA, tendrás que modificar ambos bloques. Si solo actualizas uno, el carrito y el ticket mostrarán resultados distintos.
  • El patrón «pedir el código, validarlo, pedir las unidades y validarlas» también podría aparecer en cualquier otro menú.

En 1.8 aprenderás a resolverlo con funciones: definir el cálculo del total una sola vez y llamarlo desde donde haga falta. El capítulo siguiente parte de este proyecto para mostrar esa transformación. Conserva tu solución: la retomaremos allí.

Errores frecuentes

Error Qué pasa Cómo evitarlo
Comparar la opción con un entero (opcion == 1) Nunca se cumple: input() devuelve texto Comparar con "1" o convertir con int()
int() sobre una entrada vacía o con letras ValueError y el programa se cae En este proyecto se asumen entradas numéricas; en 1.11 aprenderás a controlar este error con try/except
Asignar carrito[codigo] = unidades Pisa las unidades que ya había Acumular con carrito.get(codigo, 0) + unidades
Olvidar el continue tras un aviso El programa sigue ejecutando la opción con datos inválidos Saltar a la siguiente iteración en cuanto se detecte el problema
Cobrar sin comprobar el carrito Se emite un ticket de 0,00 € que no corresponde a ninguna compra Comprobar len(carrito) == 0 antes de emitirlo
Usar los literales 60 y 0.10 dentro del if Cambiar la política de descuento obliga a buscar esos valores por el código Definir constantes con nombre al principio del programa
Elegir los anchos de formato sin probarlos Las columnas se desplazan con nombres largos Probar con el producto de nombre más largo y ajustar los anchos

Ampliaciones

Si quieres seguir practicando, puedes ampliar el programa (todavía sin funciones):

  • Descuento por volumen: aplica un 5 % a las líneas con 3 o más unidades del mismo producto. Modifica el bucle del ticket, no el del carrito.
  • IVA: añade una línea IVA (21 %) entre la base y el total. Decide si el descuento se aplica antes o después del IVA y documenta la decisión.
  • Estadística del día: registra cuántos tickets se han emitido y cuál es su importe medio. Necesitarás acumuladores que sobrevivan al carrito = {} de cada cobro.
  • Para pensar: ¿qué ocurre si el usuario escribe letras en el código? Anótalo; aprenderás a resolverlo en 1.11 con try/except.

Producto evaluable

Entrega el script 05b_caja_registradora.py y asegúrate de que cumple estos requisitos. Es un script y no un cuaderno porque el menú lee del teclado con input(), y eso se hace en la terminal.

  1. El menú se repite hasta que se elige la opción de salir.
  2. El carrito acumula unidades al repetir el mismo producto.
  3. Cobra solo con carrito no vacío y lo vacía tras emitir el ticket.
  4. El descuento se aplica solo cuando el total lo alcanza.
  5. Con las mismas entradas, el ticket es siempre idéntico (salida estable).
  6. Incluye una tabla de «requisito → herramienta usada» (condicional, bucle, diccionario, f-string) y una conclusión sobre qué parte del código convendría no repetir, escritas como comentario al final del script o en un README.md junto a él.

Criterio de aceptación: el programa responde a entradas inválidas con un aviso y sin caerse cuando la entrada tiene el tipo esperado pero no pertenece al dominio (código inexistente, unidades menores que 1 u opción no disponible), y la sesión completa reproduce la salida esperada. Las entradas no numéricas quedan fuera de esta primera versión porque todavía no se han introducido las excepciones; se incorporarán al robustecer el programa en 1.11.

Resumen

Requisito del proyecto Herramienta que lo resuelve
Menú que se repite while True + if/elif + break
Catálogo y búsquedas Diccionario de diccionarios, in, .items()
Carrito acumulativo .get(clave, 0) + unidades
Totales Acumuladores (total += subtotal)
Descuento Condicional sobre el total, constantes con nombre
Columnas alineadas f-strings con ancho fijo (:>7.2f, :<24)
Validaciones con aviso continue tras comprobar los datos

Idea clave. Ya puedes construir un programa completo combinando las estructuras básicas. Las funciones, los ficheros y las excepciones no cambian las operaciones fundamentales; ayudan a organizarlas, reutilizarlas y controlar los errores.