Inventario de almacén con valoración FIFO y reposición
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
- Cada línea es
PRODUCTO código;nombre;mínimo;máximo,ENTRADA fecha;código;cantidad;coste unitariooSALIDA fecha;código;cantidad, con las fechas comoAAAA-MM-DD, cantidades enteras mayores que 0 y el coste con punto decimal. Los productos se declaran antes de sus movimientos. - 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 ignoradaal leerla. - 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. - 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. - 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; oReponer: nada. Para terminar,Valor del almacén: X. - 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.
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.
Ejemplo
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
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 €
Solución explicada
Ver la solución completa
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.