Volver al Blog
SoftwareDesarrolloSaaS

Desarrollo de un Sistema ERP Ligero para Inventario y Ventas con Next.js, Prisma y PostgreSQL

Brayan Developer
5 min de lectura
Desarrollo de un Sistema ERP Ligero para Inventario y Ventas con Next.js, Prisma y PostgreSQL
Aprende a diseñar y construir un ERP modular para control de stock multi-almacén, ventas POS y Kardex valorizado utilizando Next.js 15, Prisma ORM y transacciones ACID en PostgreSQL.

Las licencias de ERPs tradicionales como SAP o Oracle suelen costar decenas de miles de dólares al año y están repletas de módulos innecesarios que complican la operación de pequeñas y medianas empresas. Desarrollar un ERP ligero y a medida con Next.js, Prisma y PostgreSQL permite contar con un sistema ágil, multi-sucursal y perfectamente adaptado a las reglas de negocio exactas de tu empresa.

Sistema ERP de Inventario y Ventas

Arquitectura Modular del Sistema ERP#

Un sistema de control de inventario y facturación profesional se compone de módulos desacoplados pero interconectados bajo transacciones estrictas:

graph TD
    UI["Frontend Next.js (Dashboard + POS)"] --> Actions["Server Actions & API Routes"]
    Actions --> Auth["Autenticación RBAC (Admin / Cajero / Almacenero)"]
    Auth --> Service["Capa de Negocio (Lógica de Kardex e Inventario)"]
    
    Service --> Transac{"Transacción ACID (Prisma)"}
    Transac -->|1. Registrar Venta| Venta["Tabla 'Sale'"]
    Transac -->|2. Descontar Stock| Stock["Tabla 'ProductStock'"]
    Transac -->|3. Generar Movimiento Kardex| Kardex["Tabla 'InventoryMovement'"]
    
    Transac --> DB[("PostgreSQL")]
    DB --> PDF["Generación de Boleta/Factura PDF"]
    DB --> Notify["Alerta de Stock Crítico vía WhatsApp/Email"]

1. Modelado de Datos Relacional con Prisma ORM#

El corazón del sistema radica en la integridad referencial para evitar inconsistencias de existencias entre diferentes almacenes o tiendas físicas:

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

enum MovementType {
  ENTRY_PURCHASE
  EXIT_SALE
  TRANSFER_IN
  TRANSFER_OUT
  ADJUSTMENT_LOSS
}

model Product {
  id          String         @id @default(uuid())
  sku         String         @unique
  barcode     String?        @unique
  name        String
  description String?
  costPrice   Decimal        @db.Decimal(10, 2)
  salePrice   Decimal        @db.Decimal(10, 2)
  minStock    Int            @default(5)
  stocks      ProductStock[]
  movements   InventoryMovement[]
  createdAt   DateTime       @default(now())
}

model Warehouse {
  id        String         @id @default(uuid())
  name      String         // Ej. "Almacén Central", "Tienda Miraflores"
  location  String?
  stocks    ProductStock[]
  movements InventoryMovement[]
}

model ProductStock {
  id          String    @id @default(uuid())
  productId   String
  warehouseId String
  quantity    Int       @default(0)
  product     Product   @relation(fields: [productId], references: [id])
  warehouse   Warehouse @relation(fields: [warehouseId], references: [id])

  @@unique([productId, warehouseId])
}

model InventoryMovement {
  id          String       @id @default(uuid())
  productId   String
  warehouseId String
  type        MovementType
  quantity    Int          // Cantidad movida
  costUnit    Decimal      @db.Decimal(10, 2)
  reference   String?      // Ej. "Venta #1042" o "Guía #501"
  product     Product      @relation(fields: [productId], references: [id])
  warehouse   Warehouse    @relation(fields: [warehouseId], references: [id])
  createdAt   DateTime     @default(now())
}

2. Transacciones ACID y Control de Concurrencia al Procesar Ventas#

El mayor riesgo en un punto de venta con múltiples cajeros es la venta simultánea del último producto en stock (race condition). Con prisma.$transaction garantizamos que el descuento de inventario, el registro del Kardex y la creación de la venta ocurran en un único bloque atómico:

// server/actions/salesActions.ts
'use server';

import { prisma } from '@/lib/prisma';

interface SaleItemInput {
  productId: string;
  quantity: number;
  unitPrice: number;
}

export async function processPosSale({
  warehouseId,
  cashierId,
  items,
}: {
  warehouseId: string;
  cashierId: string;
  items: SaleItemInput[];
}) {
  return await prisma.$transaction(async (tx) => {
    let totalSale = 0;

    // 1. Validar disponibilidad y bloquear filas para actualización
    for (const item of items) {
      const stockRecord = await tx.productStock.findUnique({
        where: {
          productId_warehouseId: {
            productId: item.productId,
            warehouseId,
          },
        },
      });

      if (!stockRecord || stockRecord.quantity < item.quantity) {
        const product = await tx.product.findUnique({ where: { id: item.productId } });
        throw new Error(`Stock insuficiente para el producto: ${product?.name ?? item.productId}`);
      }

      totalSale += item.unitPrice * item.quantity;
    }

    // 2. Crear registro de venta
    const sale = await tx.sale.create({
      data: {
        warehouseId,
        cashierId,
        totalAmount: totalSale,
        status: 'COMPLETED',
      },
    });

    // 3. Descontar inventario y asentar movimientos en Kardex
    for (const item of items) {
      // Reducir stock
      await tx.productStock.update({
        where: {
          productId_warehouseId: {
            productId: item.productId,
            warehouseId,
          },
        },
        data: {
          quantity: { decrement: item.quantity },
        },
      });

      // Crear asiento en el Kardex
      await tx.inventoryMovement.create({
        data: {
          productId: item.productId,
          warehouseId,
          type: 'EXIT_SALE',
          quantity: item.quantity,
          costUnit: item.unitPrice,
          reference: `Venta POS #${sale.id.slice(0, 8)}`,
        },
      });
    }

    return { success: true, saleId: sale.id };
  });
}

3. Generación Automatizada de Reportes de Kardex y Exportación#

El sistema permite visualizar en tiempo real el método de valuación de inventario (Promedio Ponderado o PEPS/FIFO) y exportar los libros electrónicos a Excel o PDF con un clic para el área contable:

  • Detección de Quiebres de Stock: Envío inmediato de alerta a WhatsApp o Telegram cuando un artículo baja de su minStock.
  • Compatibilidad con Lectores de Códigos de Barras: Integración mediante eventos de teclado USB/Bluetooth en el frontend con retardo debounce cero.

Ventajas de un ERP a Medida frente a Soluciones Enlatadas#

Criterio Software Enlatado (Mensualidad) ERP a Medida con Next.js
Costo a Largo Plazo Suscripción recurrente eterna Un solo pago de desarrollo (100% tuyo)
Flexibilidad de Funciones Rígido, no modificable Adaptado 100% a tus flujos
Propiedad del Código y Datos Alojado en nube de terceros Tus propios servidores y base de datos
Velocidad y Usabilidad Paneles lentos y complejos Interfaz moderna, rápida y responsive

Conclusión#

Un sistema ERP a medida no es un gasto, sino una inversión estratégica que elimina fugas de dinero por inventario fantasma, agiliza la atención a clientes y proporciona visibilidad financiera total en tiempo real.

¿Tu empresa necesita digitalizar sus almacenes, ventas y facturación con un software desarrollado a medida? Escríbenos para una cotización personalizada o explora nuestras soluciones de software.

Etiquetas
ERP a MedidaNext.jsPrisma ORMPostgreSQLControl de InventarioSoftware de GestiónKardex
Compartir:XLinkedInWhatsApp

¿Te gustaría profundizar en estos temas?

Aprende sobre desarrollo de software, apps a medida, automatizaciones con N8N, Next.js y Cloud con casos reales.

Hablemos