Desarrollo de un Sistema ERP Ligero para Inventario y Ventas con Next.js, Prisma y 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.

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.
¿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.


