Código Python: clasificación ABC y rotación de inventario

Código Python: clasificación ABC y rotación de inventario
No todas las referencias de una bodega valen lo mismo. Unas pocas se llevan la mayor parte del dinero invertido en mercancía y el resto apenas ocupa estante. La clasificación ABC separa esas tres familias: las que hay que vigilar casi a diario, las que se revisan cada semana y las que basta con mirar de vez en cuando. La rotación contesta la otra pregunta del día a día: cada cuánto se renueva un producto y cuántos días de inventario se están durmiendo en la bodega. Este artículo trae un programa en Python, gratis y sin dependencias, que hace ese trabajo con su propio inventario: lee un archivo CSV, ordena los productos por valor de consumo anual, los clasifica en A, B y C, calcula la rotación y los días de inventario, y le deja la lista ordenada en un archivo que abre en Excel. Está pensado para el dueño de la ferretería o de la distribuidora, para el contador que revisa cuánto capital hay parqueado en bodega y para el almacenista que necesita saber a qué producto ponerle el ojo primero.
⬇ Descargar el código (ZIP)| Archivo del ZIP | Para qué sirve |
|---|---|
clasificacion_abc.py | El programa completo, comentado línea por línea en español. |
datos/inventario.csv | Inventario de ejemplo: diez productos de ferretería con su consumo anual, su costo unitario y su inventario promedio. |
salida_ejemplo.txt | La salida que debe obtener al ejecutarlo, para compararla con la suya. |
LEEME.md | La guía del paquete y los dos detalles de método que valen dinero. |
Qué hace el programa
- Lee el inventario desde
datos/inventario.csv: nombre del producto, unidades consumidas al año, costo unitario y unidades que en promedio se mantienen en bodega. - Calcula el valor de consumo anual de cada producto multiplicando las unidades consumidas por el costo unitario. No es lo que hay hoy en el estante: es el dinero que ese producto mueve al año, y esa cifra es la que ordena toda la lista.
- Ordena de mayor a menor y saca la participación de cada producto sobre el total y el acumulado, que es la suma corrida de arriba hacia abajo.
- Asigna la clase: A hasta el ochenta por ciento del acumulado, B hasta el noventa y cinco por ciento y C el resto. Hay un detalle fino que casi todos pasan por alto: el producto que hace cruzar el corte ya cae en la clase siguiente, así que la clase A queda algo por debajo del ochenta por ciento. Eso es normal y es más útil que forzar el número redondo.
- Calcula la rotación dividiendo el consumo anual entre el inventario promedio. Una rotación de 5 dice que ese producto se renovó cinco veces en el año; una rotación de 1 avisa que todavía queda mercancía comprada hace un año.
- Saca los días de inventario: los 360 días del año comercial divididos entre la rotación. Es la respuesta a la pregunta del jefe de bodega: cuántos días me aguanta esto.
- Imprime la tabla ordenada, el resumen por clase con sus porcentajes y dos comprobaciones de cuadre, y guarda
salida/clasificacion_abc.csvysalida/resumen_abc.csv.
Cómo ejecutarlo
Para empezar solo necesita Python 3.11 o superior instalado, con la casilla que agrega Python al PATH marcada durante la instalación. No hay que instalar nada más: el programa usa únicamente la biblioteca estándar, así que no hay que pelear con el instalador de paquetes ni con entornos virtuales.
- Descargue el ZIP con el botón verde y descomprímalo en una carpeta cualquiera.
- Abra la terminal (en Windows
cmd, en Linux o Mac cualquier terminal) y entre a esa carpeta con el comandocd. - Ejecute
python clasificacion_abc.py.
El programa ubica la carpeta donde vive el archivo, así que puede llamarlo desde cualquier parte: siempre busca los datos y escribe los resultados junto a sí mismo. Si ve errores de codificación, revise que el CSV esté guardado en UTF-8. Con el inventario de ejemplo la salida sale en un segundo, sin conexión y sin nada más instalado.
El código, explicado
Cuatro fragmentos reales, copiados tal cual del programa que viene dentro del ZIP, sin retoques de ninguna clase.
Uno: la tabla se define como datos, no como una impresión gigante. Cada columna declara su nombre interno, su ancho y el título que se ve en pantalla; de esa lista se calcula el ancho de la banderola y de cada separador:
# Ancho fijo de cada columna: así la tabla nunca se desalinea
COLUMNAS = [
("producto", 30, "Producto"),
("valor", 17, "Valor consumo"),
("participacion", 14, "Participación"),
("acumulado", 12, "Acumulado"),
("clase", 7, "Clase"),
("rotacion", 10, "Rotación"),
("dias", 9, "Días"),
]
# Lo ancha que es la banderola y cada separador de la tabla
ANCHO_LINEA = sum(ancho for _, ancho, _ in COLUMNAS) + len(COLUMNAS) - 1Fíjese en la última línea: el ancho de la tabla se calcula a partir de las columnas. Si mañana usted agrega una columna, los separadores se estiran solos y la tabla sigue alineada.
Dos: el dinero no se calcula con decimales binarios. Todo el programa trabaja con Decimal, y estos tres ayudantes formatean los importes con separador de miles y con coma decimal, además de los porcentajes:
def miles(valor: Decimal) -> str:
"""Formatea un importe con separador de miles y coma decimal (formato local)."""
entero, decimales = f"{valor:,.2f}".split(".")
return entero.replace(",", ".") + "," + decimales
def numero(valor: Decimal, decimales: int) -> str:
"""Formatea un número con los decimales pedidos y el formato local."""
entero, cola = f"{valor:,.{decimales}f}".split(".")
return entero.replace(",", ".") + "," + cola
def porcentaje(valor: Decimal) -> str:
"""Formatea un porcentaje con dos decimales y el signo de por ciento."""
return numero(valor, 2) + " %"El redondeo se hace con ROUND_HALF_UP, que es el que espera cualquiera que revise una factura a mano: los centavos se suben cuando la tercera cifra es cinco o más. Nunca use números de coma flotante para dinero, porque los centavos se desvían solos.
Tres: el corazón del cálculo. Aquí está todo lo que promete el artículo: valor de consumo, rotación, días, orden, acumulado y clase:
def calcular(filas: list[dict]) -> tuple[Decimal, list[dict]]:
"""Calcula valor, participación, acumulado, clase, rotación y días de cada producto."""
inventario = []
for fila in filas:
consumo = Decimal(fila["consumo_anual"]) # Unidades que se gastan en el año
costo = Decimal(fila["costo_unitario"]) # Lo que cuesta cada unidad
promedio = Decimal(fila["inventario_promedio"]) # Unidades que se mantienen en promedio en la bodega
# Valor de consumo: lo que vale al año todo lo que se gasta de este producto
valor = redondear(consumo * costo)
# Rotación: cuántas veces se renueva el inventario en el año
rotacion = redondear(consumo / promedio) if promedio else Decimal("0.00")
# Días de inventario: cuántos días alcanza el inventario con ese ritmo
dias = redondear(DIAS_ANO / rotacion) if rotacion else Decimal("0.00")
inventario.append({"producto": fila["producto"], "valor": valor,
"rotacion": rotacion, "dias": dias})
# De mayor a menor valor de consumo: así el acumulado tiene sentido
inventario.sort(key=lambda item: (-item["valor"], item["producto"]))
total = sum((item["valor"] for item in inventario), Decimal("0"))
acumulado = Decimal("0")
for item in inventario:
acumulado += item["valor"]
item["participacion"] = redondear(item["valor"] * CIEN / total)
item["acumulado"] = redondear(acumulado * CIEN / total)
# Clase por acumulado: A hasta el 80 por ciento, B hasta el 95 por ciento y C el resto
if item["acumulado"] <= CORTE_A:
item["clase"] = "A"
elif item["acumulado"] <= CORTE_B:
item["clase"] = "B"
else:
item["clase"] = "C"
# Por eso el producto que hace cruzar el corte ya cae en la clase siguiente
return total, inventarioVale la pena mirar tres cosas con calma. La rotación se calcula dividiendo el consumo entre el inventario promedio, y los días se sacan dividiendo el año comercial de 360 días entre esa rotación: mientras más rota un producto, menos mercancía hay que tener guardada. El orden por valor descendente es lo que le da sentido al acumulado; si la lista no está ordenada, el acumulado no dice nada. Y la clase se decide con el acumulado y no con la participación de cada producto, que es justo la diferencia entre una clasificación bien hecha y una que solo se ve bonita.
Cuatro: la salida que abre en Excel. El programa escribe dos archivos CSV y el del detalle lleva el nombre del producto junto a sus cifras ya calculadas:
def escribir_abc(inventario: list[dict]) -> None:
"""Guarda la clasificación producto por producto en un CSV."""
SALIDA_ABC.parent.mkdir(parents=True, exist_ok=True)
columnas = ["producto", "valor_consumo", "participacion",
"acumulado", "clase", "rotacion", "dias"]
with open(SALIDA_ABC, "w", encoding="utf-8", newline="") as archivo:
escritor = csv.writer(archivo)
escritor.writerow(columnas)
for item in inventario:
escritor.writerow([item["producto"], item["valor"], item["participacion"],
item["acumulado"], item["clase"], item["rotacion"], item["dias"]])En los CSV las cifras van con punto decimal, que es el formato que Excel entiende sin configurar nada, mientras que en pantalla el programa las muestra con el separador local. Así usted puede seguir trabajando en la hoja de cálculo: filtre la columna de la clase, ordene por días de inventario y tendrá su lista de prioridades hecha.
Qué vas a ver en pantalla
Esta es la salida real con el inventario de ejemplo que viaja dentro del ZIP. Primero la banderola del programa y el conteo de productos leídos:
========================================================================================================= CLASIFICACIÓN ABC Y ROTACIÓN DE INVENTARIO ========================================================================================================= Productos leídos: 10
La tabla ordenada por valor de consumo anual. El ejemplo cierra con un valor total de consumo anual de 332.270.000,00, y el cemento gris solo ya representa 110.000.000,00, es decir un tercio del total. Los porcentajes de esta tabla van escritos aquí en palabras y sin su signo, para no chocar con el editor del blog; en pantalla el programa los imprime con el signo y el CSV los guarda como números puros:
| Producto | Valor de consumo | Participación (por ciento) | Acumulado (por ciento) | Clase | Rotación | Días |
|---|---|---|---|---|---|---|
| Cemento gris 50 kg | 110.000.000,00 | 33,11 | 33,11 | A | 5,00 | 72,00 |
| Varilla corrugada 1/2 | 75.000.000,00 | 22,57 | 55,68 | A | 8,00 | 45,00 |
| Cable THHN 12 AWG | 38.400.000,00 | 11,56 | 67,23 | A | 8,00 | 45,00 |
| Pintura anticorrosiva | 34.200.000,00 | 10,29 | 77,53 | A | 3,00 | 120,00 |
| Arena de peña m3 | 27.000.000,00 | 8,13 | 85,65 | B | 3,00 | 120,00 |
| Tubo PVC 1/2 | 21.600.000,00 | 6,50 | 92,15 | B | 3,00 | 120,00 |
| Guantes de carnaza | 7.680.000,00 | 2,31 | 94,47 | B | 2,00 | 180,00 |
| Tornillo autoperforante | 6.750.000,00 | 2,03 | 96,50 | C | 1,00 | 360,00 |
| Alambre dulce kg | 6.240.000,00 | 1,88 | 98,37 | C | 1,50 | 240,00 |
| Teja de barro | 5.400.000,00 | 1,63 | 100,00 | C | 1,50 | 240,00 |
Y este es el resumen por clase, que es la parte que se lleva a la reunión con el dueño del negocio:
| Clase | Productos | Por ciento del valor | Por ciento de referencias |
|---|---|---|---|
| Clase A | 4 | 77,53 | 40,00 |
| Clase B | 3 | 16,94 | 30,00 |
| Clase C | 3 | 5,53 | 30,00 |
El cierre muestra las dos comprobaciones y los dos archivos que quedan en la carpeta de salida:
Valor total del consumo anual: 332.270.000,00 Comprobación (suma de valores del resumen = total): 332.270.000,00 = 332.270.000,00 -> CUADRA Archivo generado: salida/clasificacion_abc.csv Archivo generado: salida/resumen_abc.csv
Las comprobaciones son la parte que más gusta a los contadores: la primera verifica que el acumulado cierre exactamente en cien por ciento y la segunda, que la suma de los valores del resumen sea igual al gran total. Si alguna dijera REVISAR en lugar de CUADRA, hay un dato mal escrito en el CSV y el programa se lo está avisando antes de que usted haga cuentas sobre cifras cojas.
Errores comunes y consejos
- No encuentra el archivo de datos. El programa espera
datos/inventario.csvjunto al archivo del programa. Si descomprimió el ZIP y luego movió solo el .py, vuelva a armar la carpeta con su subcarpetadatos. - Cifras que no cuadran. Revise que el consumo anual esté en unidades y el costo unitario en pesos por unidad. Si mezcla cajas con unidades, la rotación sale por las nubes y las clases quedan al revés.
- Inventario promedio en cero. Un producto con promedio cero no se puede dividir: el programa lo deja en cero para no caerse, pero ese dato hay que corregirlo en el origen.
- Separador decimal. Los datos de ejemplo usan punto decimal. Si su sistema exporta con coma decimal, ajuste los datos antes de correr el programa.
- Pocas referencias. Con una lista muy corta el ABC pierde gracia, porque casi todo cae en la clase A. Úselo con el inventario completo del negocio.
- Un solo periodo. El valor de consumo es una foto del último año. Si el negocio es estacional, corra el programa con cada trimestre y compare.
Cuándo esto no alcanza
Este programa es gratis y sirve para ordenar el inventario y para entender las cifras: cópielo, modifíquelo y úselo con sus datos cuando quiera. Lo que no hace es llevar el día a día, porque no registra entradas ni salidas, no descuenta del saldo cuando alguien vende y no le avisa cuando un producto se agota. Cuando el negocio pasa de unos pocos movimientos diarios, el archivo y el script se quedan cortos y toca pensar en un sistema. Kardex Tauro existe justo para eso, para cuando ya no alcanza con Excel y con scripts: lleva la ficha de cada producto, calcula el saldo y le deja el inventario ordenado. Pero antes de mirar cualquier sistema, corra este programa: con la clasificación en la mano usted sabe qué productos justifican el software y cuáles no.
⬇ Descargar el código (ZIP)Descargue el ZIP, reemplace el inventario de ejemplo por el suyo y ejecute el programa. En menos de lo que se demora un café usted va a saber qué productos sostienen su negocio y cuáles le están comiendo el capital.
Material didáctico de control interno escrito para el taller de código de este blog. No es asesoría contable, tributaria ni legal: revise sus cifras y sus normas locales antes de tomar decisiones con estos números.