Apuntes DAM
Volver al inicio

Inventario de almacén con valoración FIFO y reposición

Ejercicio de PythonMuy difícilUnos 90 minutos

El cálculo de stock de un ERP: aplica entradas y salidas de almacén por fecha, valora las existencias y el coste de lo vendido por el método FIFO (los lotes más antiguos salen primero), rechaza salidas sin stock y propone pedidos de reposición. Decimal, listas como colas y ordenación de tuplas.

  • Decimal para dinero
  • Listas como colas (lotes)
  • Ordenar tuplas
  • try / except
  • Diccionarios anidados
  • partition y split

Enunciado

En un ERP como Odoo, el módulo de inventario no solo cuenta unidades: también valora el almacén. Si un tornillo se ha comprado una vez a 0,05 € y otra a 0,06 €, ¿cuánto valen los que quedan y cuánto costaron los vendidos? La respuesta depende del método de valoración, y uno de los más usados es FIFO (primero en entrar, primero en salir): cada salida se lleva primero las unidades del lote más antiguo.

Los movimientos del almacén llegan de varios terminales y no siempre en orden, así que hay que aplicarlos por fecha. Una salida de más unidades de las que hay se rechaza (es un error de la persona que la registró) y no cambia nada.

Además, el ERP propone pedidos: cada producto tiene un stock mínimo y uno máximo, y cuando queda por debajo del mínimo hay que pedir lo necesario para volver al máximo.

Qué tiene que hacer el programa

  1. Cada línea es PRODUCTO código;nombre;mínimo;máximo, ENTRADA fecha;código;cantidad;coste unitario o SALIDA fecha;código;cantidad, con las fechas como AAAA-MM-DD, cantidades enteras mayores que 0 y el coste con punto decimal. Los productos se declaran antes de sus movimientos.
  2. Una línea con otro formato, un número no válido, un producto que no existe (o repetido) o un mínimo mayor que el máximo escribe Aviso: línea N ignorada al leerla.
  3. Los movimientos se aplican por fecha y, si coinciden, en el orden de la entrada. Una entrada crea un lote con su cantidad y su coste. Una salida con más unidades que el stock escribe Aviso: salida de C código el fecha rechazada (stock S) y no se aplica; si hay stock, consume unidades del lote más antiguo, y cuando se acaba, del siguiente, sumando su coste al coste de lo vendido.
  4. Escribe Inventario: y, por cada producto en el orden en que se declararon, código nombre: stock S · valor V · coste de lo vendido C, donde el valor es la suma de las unidades que quedan por el coste de su lote.
  5. Después, Reponer: y una línea por producto con stock menor que su mínimo, código nombre: pedir P (stock S, mínimo m, máximo M) con P = máximo − stock; o Reponer: nada. Para terminar, Valor del almacén: X.
  6. Los importes se calculan con decimales exactos, se redondean al céntimo solo al escribirlos (las mitades suben) y se escriben con punto de millar, coma decimal y €. Si no hay ningún producto, No hay productos.

Entrada

Una línea por producto o movimiento: PRODUCTO …, ENTRADA … o SALIDA ….

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Tornillos y tuercas

Entrada

PRODUCTO T01;Tornillo M6;100;500
PRODUCTO T02;Tuerca M6;100;400
ENTRADA 2026-09-03;T01;200;0.06
ENTRADA 2026-09-01;T01;300;0.05
SALIDA 2026-09-05;T01;350
ENTRADA 2026-09-02;T02;150;0.035
SALIDA 2026-09-06;T02;80
SALIDA 2026-09-07;T02;100

Salida por consola

Aviso: salida de 100 T02 el 2026-09-07 rechazada (stock 70)
Inventario:
  T01 Tornillo M6: stock 150 · valor 9,00 € · coste de lo vendido 18,00 €
  T02 Tuerca M6: stock 70 · valor 2,45 € · coste de lo vendido 2,80 €
Reponer:
  T02 Tuerca M6: pedir 330 (stock 70, mínimo 100, máximo 400)
Valor del almacén: 11,45 €

Líneas incorrectas

Entrada

PRODUCTO A;Arandela;10;50
PRODUCTO A;Repetido;1;2
PRODUCTO B;Bisagra;20;10
ENTRADA 2026-10-01;A;40;0.10
SALIDA 2026-10-01;A;15
SALIDA 2026-10-01;A;30
SALIDA 2026-10-02;Z;1
ENTRADA 2026-10-03;A;-5;0.10
AJUSTE 2026-10-03;A;3

Salida por consola

Aviso: línea 2 ignorada
Aviso: línea 3 ignorada
Aviso: línea 7 ignorada
Aviso: línea 8 ignorada
Aviso: línea 9 ignorada
Aviso: salida de 30 A el 2026-10-01 rechazada (stock 25)
Inventario:
  A Arandela: stock 25 · valor 2,50 € · coste de lo vendido 1,50 €
Reponer: nada
Valor del almacén: 2,50 €

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Leer con partition y split

linea.partition(" ") separa el tipo del resto de la línea, y resto.split(";") da los campos. Convierte los números dentro de un try: int("abc") y Decimal("x") lanzan excepciones que convertirás en el aviso.

2. Ordenar los movimientos sin perder el orden de la entrada

Guarda cada movimiento como una tupla que empiece por la fecha y el número de línea. Al ordenar tuplas, Python compara el primer elemento y, si empata, el segundo: la fecha AAAA-MM-DD se ordena bien como texto, y el número de línea resuelve los empates.

python
for fecha, _, tipo, codigo, cantidad, coste in sorted(movimientos):
3. Los lotes como una cola

Cada producto tiene una lista de lotes [cantidad, coste] en el orden en que entraron. Una entrada añade al final; una salida consume desde el principio: toma min(lo que falta, lo que queda en el lote), suma su coste, y si el lote se vacía lo saca con pop(0).

4. Decimal para el dinero

Decimal("0.05") * 300 es exactamente 15,00; con float saldría 15.000000000000002. Redondea solo al escribir con quantize(Decimal("0.01"), rounding=ROUND_HALF_UP).

5. Valor y reposición

El valor del stock es la suma de cantidad × coste de los lotes que quedan. Para la reposición, compara el stock con el mínimo y calcula lo que falta hasta el máximo.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

🐍PythonInventario de almacén con valoración FIFO y reposiciónMuy difícil

Ejemplo

Entrada (lo que se escribe por teclado)
PRODUCTO T01;Tornillo M6;100;500
PRODUCTO T02;Tuerca M6;100;400
ENTRADA 2026-09-03;T01;200;0.06
ENTRADA 2026-09-01;T01;300;0.05
SALIDA 2026-09-05;T01;350
ENTRADA 2026-09-02;T02;150;0.035
SALIDA 2026-09-06;T02;80
SALIDA 2026-09-07;T02;100
Salida esperada
Aviso: salida de 100 T02 el 2026-09-07 rechazada (stock 70)
Inventario:
  T01 Tornillo M6: stock 150 · valor 9,00 € · coste de lo vendido 18,00 €
  T02 Tuerca M6: stock 70 · valor 2,45 € · coste de lo vendido 2,80 €
Reponer:
  T02 Tuerca M6: pedir 330 (stock 70, mínimo 100, máximo 400)
Valor del almacén: 11,45 €
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
python
1import sys
2from decimal import Decimal, ROUND_HALF_UP
3
4
5def euros(x):
6    """Decimal → «1.234,56 €» redondeado al céntimo."""
7    x = x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
8    return f"{x:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + " €"
9
10
11def main():
12    productos = {}       # código → datos del producto
13    movimientos = []     # (fecha, orden de la entrada, tipo, código, cantidad, coste)
14    for num, linea in enumerate(sys.stdin.read().split("\n"), start=1):
15        linea = linea.strip()
16        if not linea:
17            continue
18        tipo, _, resto = linea.partition(" ")
19        campos = [c.strip() for c in resto.split(";")]
20        try:
21            if tipo == "PRODUCTO" and len(campos) == 4:
22                codigo, nombre, minimo, maximo = campos[0], campos[1], int(campos[2]), int(campos[3])
23                if codigo in productos or not 0 <= minimo <= maximo:
24                    raise ValueError
25                productos[codigo] = {"nombre": nombre, "minimo": minimo, "maximo": maximo, "lotes": [], "vendido": Decimal(0)}
26            elif tipo == "ENTRADA" and len(campos) == 4:
27                cantidad, coste = int(campos[2]), Decimal(campos[3])
28                if cantidad <= 0 or coste < 0 or campos[1] not in productos:
29                    raise ValueError
30                movimientos.append((campos[0], num, tipo, campos[1], cantidad, coste))
31            elif tipo == "SALIDA" and len(campos) == 3:
32                cantidad = int(campos[2])
33                if cantidad <= 0 or campos[1] not in productos:
34                    raise ValueError
35                movimientos.append((campos[0], num, tipo, campos[1], cantidad, None))
36            else:
37                raise ValueError
38        except (ValueError, ArithmeticError):
39            print(f"Aviso: línea {num} ignorada")
40
41    # Los movimientos llegan desordenados: se aplican por fecha (y, el mismo día, en el orden de la entrada)
42    for fecha, _, tipo, codigo, cantidad, coste in sorted(movimientos):
43        p = productos[codigo]
44        if tipo == "ENTRADA":
45            p["lotes"].append([cantidad, coste])
46            continue
47        stock = sum(c for c, _ in p["lotes"])
48        if cantidad > stock:
49            print(f"Aviso: salida de {cantidad} {codigo} el {fecha} rechazada (stock {stock})")
50            continue
51        # FIFO: se gastan primero las unidades de los lotes más antiguos, a su coste
52        while cantidad:
53            lote = p["lotes"][0]
54            usadas = min(cantidad, lote[0])
55            p["vendido"] += usadas * lote[1]
56            lote[0] -= usadas
57            cantidad -= usadas
58            if lote[0] == 0:
59                p["lotes"].pop(0)
60
61    if not productos:
62        print("No hay productos")
63        return
64    total = Decimal(0)
65    reponer = []
66    print("Inventario:")
67    for codigo, p in productos.items():
68        stock = sum(c for c, _ in p["lotes"])
69        valor = sum((c * coste for c, coste in p["lotes"]), Decimal(0))
70        total += valor
71        print(f"  {codigo} {p['nombre']}: stock {stock} · valor {euros(valor)} · coste de lo vendido {euros(p['vendido'])}")
72        if stock < p["minimo"]:
73            reponer.append(f"  {codigo} {p['nombre']}: pedir {p['maximo'] - stock} (stock {stock}, mínimo {p['minimo']}, máximo {p['maximo']})")
74    print("Reponer:" if reponer else "Reponer: nada")
75    for r in reponer:
76        print(r)
77    print(f"Valor del almacén: {euros(total)}")
78
79
80main()

Separar la lectura (que valida y guarda) de la aplicación (que ordena y ejecuta) permite procesar los movimientos en el orden correcto aunque lleguen desordenados, y que un error de formato no deje el almacén a medias.

La lista de lotes de cada producto es una cola: FIFO literalmente. El bucle de la salida recorre los lotes desde el más antiguo y reparte la cantidad entre ellos, que es lo que hace el ERP para calcular el coste de cada venta.

Con Decimal todos los cálculos son exactos y el redondeo ocurre una sola vez, al mostrar el resultado. Es lo que exige la contabilidad: el valor del almacén tiene que cuadrar con la suma de lo comprado menos lo vendido.

El coste de lo vendido depende del método: con FIFO las salidas se valoran a los precios antiguos, y si los precios suben, el stock que queda vale más. Es una decisión contable de la empresa, y por eso los ERP permiten elegir entre FIFO, coste medio y otros.

Para ir más allá

  • Añade la valoración por coste medio ponderado y compara el resultado con FIFO para los mismos movimientos.
  • Admite devoluciones de clientes (entradas al coste de la salida original) y regularizaciones de inventario.
  • Exporta los pedidos de reposición en CSV agrupados por proveedor.

Dónde se explica