Proyectos · C# · Intermedio

Inventario de una tienda

Un programa que lleva el inventario de una tienda: avisa de lo que hay que reponer, calcula el valor por categorías, registra ventas sin quedarse en negativo y guarda todo en JSON.

  • 5 pasos
  • 1 hora y cuarto aproximadamente
  • 52 líneas de código

Lo que vas a construir

Con LINQ, las preguntas típicas de un negocio («¿qué tengo que reponer?», «¿cuánto vale lo que tengo en el almacén?») se responden en una línea. Vas a modelar los productos con un record, consultar el inventario con LINQ, registrar ventas tratando los errores y guardar los datos en JSON para la siguiente vez.

Lo que vas a practicar

  • Datos inmutables con record
  • Consultas con LINQ: Where, OrderBy y Sum
  • Agrupar con GroupBy
  • Excepciones con filtro when y la expresión with
  • Guardar y cargar con System.Text.Json

Cómo ejecutarlo: Crea un proyecto con dotnet new console -o Inventario, sustituye el contenido de Program.cs por el código de cada paso y ejecútalo con dotnet run dentro de la carpeta. Los decimales se muestran con coma o punto según el idioma del sistema.

Así funciona al final
Primer uso: inventario de ejemplo.
Vendidas 1 uds. de Teclado. Quedan 11.
Guardado en inventario.json.

Antes de empezar, conviene haber hecho estas lecciones

Paso 1 de 5

Los productos como record

record Producto(...) define en una línea un tipo con cuatro propiedades de solo lectura. Con decimal (el sufijo m), los precios no tienen los errores de redondeo de double, por eso es el tipo recomendado para dinero.

En la interpolación, {p.Nombre,-18} ocupa 18 caracteres alineado a la izquierda y {p.Precio,9:0.00}, 9 a la derecha con dos decimales: el listado sale en columnas.

Program.cs
var inventario = new List<Producto>
{
    new("Teclado", "Informática", 35.00m, 12),
    new("Ratón", "Informática", 12.50m, 3),
    new("Monitor 24\"", "Informática", 129.90m, 4),
    new("Cuaderno", "Papelería", 2.40m, 40),
    new("Bolígrafos (10)", "Papelería", 3.95m, 2),
    new("Lámpara LED", "Hogar", 19.99m, 7),
};

foreach (var p in inventario)
{
    Console.WriteLine($"{p.Nombre,-18}{p.Precio,9:0.00} €  ({p.Stock} uds.)");
}

record Producto(string Nombre, string Categoria, decimal Precio, int Stock);
Al ejecutarlo
Teclado               35,00 €  (12 uds.)
Ratón                 12,50 €  (3 uds.)
Monitor 24"          129,90 €  (4 uds.)
Cuaderno               2,40 €  (40 uds.)
Bolígrafos (10)        3,95 €  (2 uds.)
Lámpara LED           19,99 €  (7 uds.)

Paso 2 de 5

Qué reponer y cuánto vale todo

Where se queda con los productos que cumplen la condición y OrderBy los ordena: los que menos stock tienen salen primero.

Sum(p => p.Precio * p.Stock) multiplica precio por unidades de cada producto y lo suma todo: el valor del inventario en una línea.

Program.csLo nuevo, resaltado
var inventario = new List<Producto>
{
    new("Teclado", "Informática", 35.00m, 12),
    new("Ratón", "Informática", 12.50m, 3),
    new("Monitor 24\"", "Informática", 129.90m, 4),
    new("Cuaderno", "Papelería", 2.40m, 40),
    new("Bolígrafos (10)", "Papelería", 3.95m, 2),
    new("Lámpara LED", "Hogar", 19.99m, 7),
};

// Productos con menos de 5 unidades: hay que pedir más
var aReponer = inventario.Where(p => p.Stock < 5).OrderBy(p => p.Stock);
Console.WriteLine("Reponer pronto:");
foreach (var p in aReponer)
{
    Console.WriteLine($"  {p.Nombre} ({p.Stock} uds.)");
}

decimal valorTotal = inventario.Sum(p => p.Precio * p.Stock);
Console.WriteLine($"Valor del inventario: {valorTotal:0.00} €");

record Producto(string Nombre, string Categoria, decimal Precio, int Stock);
Al ejecutarlo
Reponer pronto:
  Bolígrafos (10) (2 uds.)
  Ratón (3 uds.)
  Monitor 24" (4 uds.)
Valor del inventario: 1220,93 €

Paso 3 de 5

Resumen por categorías

GroupBy(p => p.Categoria) reparte los productos en grupos; cada grupo tiene su clave (g.Key) y sus elementos, con los que se cuentan los productos y se suma su valor.

Select crea un objeto anónimo con esos datos y OrderByDescending pone primero la categoría de más valor.

Program.csLo nuevo, resaltado
var inventario = new List<Producto>
{
    new("Teclado", "Informática", 35.00m, 12),
    new("Ratón", "Informática", 12.50m, 3),
    new("Monitor 24\"", "Informática", 129.90m, 4),
    new("Cuaderno", "Papelería", 2.40m, 40),
    new("Bolígrafos (10)", "Papelería", 3.95m, 2),
    new("Lámpara LED", "Hogar", 19.99m, 7),
};

var porCategoria = inventario
    .GroupBy(p => p.Categoria)
    .Select(g => new { Categoria = g.Key, Productos = g.Count(), Valor = g.Sum(p => p.Precio * p.Stock) })
    .OrderByDescending(r => r.Valor);

Console.WriteLine($"{"Categoría",-14}{"Productos",10}{"Valor",12}");
foreach (var r in porCategoria)
{
    Console.WriteLine($"{r.Categoria,-14}{r.Productos,10}{r.Valor,10:0.00} €");
}

record Producto(string Nombre, string Categoria, decimal Precio, int Stock);
Al ejecutarlo
Categoría      Productos       Valor
Informática            3    977,10 €
Hogar                  1    139,93 €
Papelería              2    103,90 €

Paso 4 de 5

Registrar ventas sin errores

Vender comprueba que el producto exista y que haya unidades suficientes; si no, lanza una excepción con un mensaje claro. El catch con when captura solo esos dos tipos de error: cualquier otro fallo inesperado no se ocultaría.

Un record no se modifica: la expresión producto with { Stock = ... } crea una copia con el stock cambiado, que sustituye a la anterior en la lista.

Program.csLo nuevo, resaltado
var inventario = new List<Producto>
{
    new("Teclado", "Informática", 35.00m, 12),
    new("Ratón", "Informática", 12.50m, 3),
    new("Monitor 24\"", "Informática", 129.90m, 4),
    new("Cuaderno", "Papelería", 2.40m, 40),
    new("Bolígrafos (10)", "Papelería", 3.95m, 2),
    new("Lámpara LED", "Hogar", 19.99m, 7),
};

Vender("Ratón", 2);
Vender("Ratón", 2);
Vender("Grapadora", 1);

void Vender(string nombre, int cantidad)
{
    try
    {
        int posicion = inventario.FindIndex(p => p.Nombre == nombre);
        if (posicion == -1)
            throw new KeyNotFoundException($"No existe el producto «{nombre}»");

        var producto = inventario[posicion];
        if (producto.Stock < cantidad)
            throw new InvalidOperationException($"Solo quedan {producto.Stock} uds. de {nombre}");

        // Los records no se modifican: «with» crea una copia con el stock cambiado
        inventario[posicion] = producto with { Stock = producto.Stock - cantidad };
        Console.WriteLine($"Vendidas {cantidad} uds. de {nombre}. Quedan {inventario[posicion].Stock}.");
    }
    catch (Exception e) when (e is KeyNotFoundException or InvalidOperationException)
    {
        Console.WriteLine($"No se puede vender: {e.Message}");
    }
}

record Producto(string Nombre, string Categoria, decimal Precio, int Stock);
Al ejecutarlo
Vendidas 2 uds. de Ratón. Quedan 1.
No se puede vender: Solo quedan 1 uds. de Ratón
No se puede vender: No existe el producto «Grapadora»

Paso 5 de 5

Guardar el inventario en JSON

Si existe inventario.json, JsonSerializer.Deserialize lo convierte de nuevo en una lista de productos; si no, se empieza con los datos de ejemplo. Al terminar, Serialize guarda la lista con sangría (WriteIndented) para que se pueda leer.

La segunda vez que lo ejecutes, cargará el inventario guardado y el teclado tendrá una unidad menos cada vez.

Program.csLo nuevo, resaltado
using System.Text.Json;

const string Archivo = "inventario.json";
var opciones = new JsonSerializerOptions { WriteIndented = true };

List<Producto> inventario;
if (File.Exists(Archivo))
{
    inventario = JsonSerializer.Deserialize<List<Producto>>(File.ReadAllText(Archivo)) ?? new();
    Console.WriteLine($"Inventario cargado: {inventario.Count} productos.");
}
else
{
    inventario = new List<Producto>
    {
        new("Teclado", "Informática", 35.00m, 12),
        new("Ratón", "Informática", 12.50m, 3),
        new("Monitor 24\"", "Informática", 129.90m, 4),
        new("Cuaderno", "Papelería", 2.40m, 40),
        new("Bolígrafos (10)", "Papelería", 3.95m, 2),
        new("Lámpara LED", "Hogar", 19.99m, 7),
    };
    Console.WriteLine("Primer uso: inventario de ejemplo.");
}

Vender("Teclado", 1);

File.WriteAllText(Archivo, JsonSerializer.Serialize(inventario, opciones));
Console.WriteLine($"Guardado en {Archivo}.");

void Vender(string nombre, int cantidad)
{
    try
    {
        int posicion = inventario.FindIndex(p => p.Nombre == nombre);
        if (posicion == -1)
            throw new KeyNotFoundException($"No existe el producto «{nombre}»");

        var producto = inventario[posicion];
        if (producto.Stock < cantidad)
            throw new InvalidOperationException($"Solo quedan {producto.Stock} uds. de {nombre}");

        inventario[posicion] = producto with { Stock = producto.Stock - cantidad };
        Console.WriteLine($"Vendidas {cantidad} uds. de {nombre}. Quedan {inventario[posicion].Stock}.");
    }
    catch (Exception e) when (e is KeyNotFoundException or InvalidOperationException)
    {
        Console.WriteLine($"No se puede vender: {e.Message}");
    }
}

record Producto(string Nombre, string Categoria, decimal Precio, int Stock);
Al ejecutarlo
Primer uso: inventario de ejemplo.
Vendidas 1 uds. de Teclado. Quedan 11.
Guardado en inventario.json.

Retos para seguir

Has terminado el proyecto. Si quieres ir más allá, prueba a añadirle esto por tu cuenta:

  1. Añade un menú en consola para vender, reponer y listar sin cambiar el código.
  2. Registra cada venta con su fecha en otro archivo y calcula las ventas del día.
  3. Avisa por consola de los productos cuyo stock baje de 5 justo después de una venta.

Conceptos clave

Repasa la ruta de C# o elige otro proyecto.