Introducción
El módulo de compras permite registrar adquisiciones de productos a proveedores, actualizando automáticamente el inventario y generando cuentas por pagar cuando corresponda.
Las compras SIEMPRE aumentan el stock automáticamente al guardar. No hay opción de desactivar esta funcionalidad.
Estructura de una Compra
Datos principales
// Tabla: compras
[
'id_compra' ,
'id_tido' , // Tipo de documento (factura, boleta, etc.)
'serie' , // Serie del documento del proveedor
'numero' , // Número del documento del proveedor
'id_proveedor' , // FK a tabla proveedores
'fecha_emision' ,
'fecha_vencimiento' , // Opcional, para créditos
'id_tipo_pago' , // 1=Contado, 2=Crédito
'moneda' , // PEN, USD
'subtotal' ,
'igv' , // Usualmente 0 en compras
'total' ,
'direccion' ,
'observaciones' ,
'estado' , // 1=Activo, 0=Anulado
'id_empresa' ,
'id_usuario' ,
]
Detalle de productos
// Tabla: detalle_compras
[
'id' ,
'id_compra' ,
'id_producto' ,
'cantidad' ,
'precio' , // Precio de compra unitario
'costo' , // = precio (se duplica por compatibilidad)
'subtotal' , // cantidad * precio
]
Flujo Paso a Paso
Acceder al Formulario de Compra
Ruta: /compras/crearComponente: CompraForm.jsxEl formulario se divide en dos columnas:
Izquierda: Selector de productos y tabla de items
Derecha: Datos del proveedor y configuración de pago
Seleccionar Proveedor
Componente: ProveedorAutocomplete.jsxBúsqueda de proveedor existente // Autocomplete busca por:
- RUC
- Razón social
// Retorna:
{
proveedor_id : 123 ,
ruc : '20123456789' ,
razon_social : 'DISTRIBUIDORA XYZ SAC' ,
direccion : 'Jr. Los Pinos 123' ,
telefono : '01-1234567' ,
email : '[email protected] '
}
Crear proveedor nuevo Si no existe en la base de datos:
Click en ”+ Nuevo Proveedor”
Ingresar RUC (consulta automática API Perú)
Completar datos: razón social, dirección, contacto
Guardar → Proveedor se crea y selecciona automáticamente
Los proveedores se guardan en tabla proveedores con scope de id_empresa.
Configurar Datos del Documento
Tipo de Documento Select con opciones:
Factura (id_tido = 2)
Boleta (id_tido = 1)
Nota de crédito (id_tido = 7)
Nota de débito (id_tido = 8)
Desde: Tabla documentos_sunatSerie y Número IMPORTANTE: Serie y número corresponden al documento emitido por el proveedor, NO son generados por el sistema.
// CompraForm.jsx
const [ formData , setFormData ] = useState ({
tipo_doc: '2' , // Factura por defecto
serie: '' , // Ej: F001, B002
numero: '' , // Ej: 00001234
fecha_emision: new Date (). toISOString (). split ( 'T' )[ 0 ],
fecha_vencimiento: null ,
id_tipo_pago: '1' , // Contado por defecto
moneda: 'PEN' ,
});
Validación de duplicados: // CompraController.php línea 114-126
$existe = Compra :: where ( 'id_empresa' , $idEmpresa )
-> where ( 'id_proveedor' , $request -> id_proveedor )
-> where ( 'id_tido' , $request -> tipo_doc )
-> where ( 'serie' , $request -> serie )
-> where ( 'numero' , $request -> numero )
-> exists ();
if ( $existe ) {
return response () -> json ([
'success' => false ,
'message' => 'Ya existe una compra registrada con este documento del proveedor'
], 400 );
}
Fechas
Fecha de emisión: Obligatoria, fecha del documento del proveedor
Fecha de vencimiento: Opcional, solo si es crédito
Moneda
PEN: Soles peruanos (símbolo: S/)
USD: Dólares americanos (símbolo: $)
El sistema NO realiza conversión de moneda automática. Los costos se guardan en la moneda seleccionada.
Agregar Productos
Opción 1: Búsqueda Individual Componente: ProductoFormSection.jsx// Proceso:
1. Buscar producto por código o nombre
2. Ingresar cantidad
3. Ingresar costo unitario ( precio de compra )
4. Click "Agregar"
// Calcula automáticamente:
subtotal = cantidad * costo
ProductoAutocomplete busca en: SELECT * FROM productos
WHERE id_empresa = {empresa_id}
AND almacen = '1' -- Solo productos de catálogo
AND (
codigo LIKE '%{query}%'
OR nombre LIKE '%{query}%'
)
LIMIT 10 ;
Opción 2: Búsqueda Múltiple Modal: ProductMultipleSearch.jsx
Click “Búsqueda Múltiple”
Se abre modal con tabla de productos
Filtros disponibles:
Buscar por código/nombre
Filtrar por categoría
Ordenar por stock/precio
Seleccionar múltiples productos (checkboxes)
Click “Agregar Seleccionados”
Modal para ingresar cantidad y costo de cada uno
Tabla de Productos Componente: ProductosTable.jsxMuestra:
Código
Descripción
Cantidad
Costo unitario (editable inline)
Subtotal
Acciones: Editar, Eliminar
// Edición inline de cantidad y costo
const handleUpdateProductField = ( index , field , value ) => {
const updated = [ ... productos ];
updated [ index ][ field ] = value ;
// Recalcular subtotal
if ( field === 'cantidad' || field === 'costo' ) {
updated [ index ]. subtotal =
updated [ index ]. cantidad * updated [ index ]. costo ;
}
setProductos ( updated );
};
Configurar Tipo de Pago
Contado (id_tipo_pago=1)
Pago inmediato
No requiere configuración adicional
No genera cuentas por pagar
Crédito (id_tipo_pago=2) Requiere:
Fecha de vencimiento
Programación de cuotas
Modal de Cuotas: PaymentSchedule.jsx// Configuración de cuotas
{
tieneInicial : false , // Sin monto inicial
montoInicial : 0 ,
cuotas : [
{
numero: 1 ,
monto: 500.00 ,
fecha: '2025-04-06' ,
tipo: 'cuota'
},
{
numero: 2 ,
monto: 500.00 ,
fecha: '2025-05-06' ,
tipo: 'cuota'
}
]
}
Validación: const totalCuotas = cuotas . reduce (( sum , c ) => sum + parseFloat ( c . monto ), 0 );
if ( Math . abs ( totalCuotas - total ) > 0.01 ) {
toast . error ( 'La suma de cuotas debe ser igual al total de la compra' );
return ;
}
Guarda en tabla: dias_compras// CompraController.php línea 203-212
if ( $request -> id_tipo_pago == 2 && ! empty ( $request -> cuotas )) {
foreach ( $request -> cuotas as $cuota ) {
DiaCompra :: create ([
'id_compra' => $compra -> id_compra ,
'monto' => $cuota [ 'monto' ],
'fecha' => $cuota [ 'fecha' ],
'estado' => '1' // 1=Pendiente, 0=Pagado
]);
}
}
Guardar Compra
Endpoint: POST /api/comprasProceso en Backend: // CompraController.php línea 70-232
public function store ( Request $request )
{
DB :: beginTransaction ();
try {
// 1. Validar datos
$request -> validate ([
'id_proveedor' => 'required|exists:proveedores,proveedor_id' ,
'tipo_doc' => 'required|exists:documentos_sunat,id_tido' ,
'serie' => 'required|string|max:4' ,
'numero' => 'required|string|max:8' ,
'productos' => 'required|array|min:1' ,
]);
// 2. Calcular totales
$subtotal = 0 ;
foreach ( $request -> productos as $prod ) {
$subtotal += $prod [ 'cantidad' ] * $prod [ 'costo' ];
}
$igv = 0 ; // Las compras no llevan IGV separado
$total = $subtotal + $igv ;
// 3. Crear compra
$compra = Compra :: create ([
'id_tido' => $request -> tipo_doc ,
'serie' => $request -> serie ,
'numero' => $request -> numero ,
'id_proveedor' => $request -> id_proveedor ,
'proveedor_id' => $request -> id_proveedor ,
'fecha_emision' => $request -> fecha_emision ,
'fecha_vencimiento' => $request -> fecha_vencimiento ,
'id_tipo_pago' => $request -> id_tipo_pago ,
'moneda' => $request -> moneda ,
'subtotal' => $subtotal ,
'igv' => $igv ,
'total' => $total ,
'id_empresa' => $idEmpresa ,
'id_usuario' => $idUsuario ,
'estado' => '1'
]);
// 4. Guardar productos y actualizar stock
foreach ( $request -> productos as $prod ) {
// Guardar detalle
ProductoCompra :: create ([
'id_compra' => $compra -> id_compra ,
'id_producto' => $prod [ 'id_producto' ],
'cantidad' => $prod [ 'cantidad' ],
'precio' => $prod [ 'costo' ],
'costo' => $prod [ 'costo' ]
]);
// INCREMENTAR STOCK
$producto = Producto :: find ( $prod [ 'id_producto' ]);
$stockAnterior = $producto -> cantidad ;
$stockNuevo = $stockAnterior + $prod [ 'cantidad' ];
$producto -> cantidad = $stockNuevo ;
$producto -> costo = $prod [ 'costo' ]; // Actualizar costo del producto
$producto -> save ();
// Registrar movimiento de stock
MovimientoStock :: create ([
'id_producto' => $prod [ 'id_producto' ],
'tipo_movimiento' => 'entrada' ,
'cantidad' => $prod [ 'cantidad' ],
'stock_anterior' => $stockAnterior ,
'stock_nuevo' => $stockNuevo ,
'tipo_documento' => 'compra' ,
'id_documento' => $compra -> id_compra ,
'documento_referencia' => $compra -> serie . '-' . $compra -> numero ,
'motivo' => 'Compra a proveedor' ,
'id_almacen' => 1 ,
'id_empresa' => $idEmpresa ,
'id_usuario' => $idUsuario ,
'fecha_movimiento' => now ()
]);
}
// 5. Guardar cuotas si es crédito
if ( $request -> id_tipo_pago == 2 && ! empty ( $request -> cuotas )) {
foreach ( $request -> cuotas as $cuota ) {
DiaCompra :: create ([
'id_compra' => $compra -> id_compra ,
'monto' => $cuota [ 'monto' ],
'fecha' => $cuota [ 'fecha' ],
'estado' => '1'
]);
}
}
DB :: commit ();
return response () -> json ([
'success' => true ,
'message' => 'Compra registrada exitosamente' ,
'data' => [
'id_compra' => $compra -> id_compra ,
'documento' => $compra -> serie . '-' . $compra -> numero
]
]);
} catch ( Exception $e ) {
DB :: rollBack ();
return response () -> json ([
'success' => false ,
'message' => 'Error al guardar compra: ' . $e -> getMessage ()
], 500 );
}
}
Impresión y Confirmación
Después de guardar exitosamente, aparece modal PrintOptionsModal: Opciones disponibles:
Ver PDF de Compra
Ruta: /reporteCompra/a4.php?id={compra_id}
Formato A4 con detalles completos
Ver Lista de Compras
Nueva Compra
Limpia formulario para siguiente registro
PDF generado incluye:
Datos del proveedor (RUC, razón social, dirección)
Documento de referencia (serie-número)
Tabla de productos con cantidades y costos
Totales (subtotal, IGV si aplica, total)
Tipo de pago y cuotas (si es crédito)
Fecha de emisión y vencimiento
Gestión de Cuentas por Pagar
Para compras a crédito Módulo: /finanzas/cuentas-por-pagarQuery para obtener cuentas: $cuentasPorPagar = DB :: table ( 'dias_compras as dc' )
-> join ( 'compras as c' , 'dc.id_compra' , '=' , 'c.id_compra' )
-> join ( 'proveedores as p' , 'c.id_proveedor' , '=' , 'p.proveedor_id' )
-> where ( 'c.id_empresa' , $idEmpresa )
-> where ( 'dc.estado' , '1' ) // Solo pendientes
-> select ([
'dc.id' ,
'p.razon_social' ,
'c.serie' ,
'c.numero' ,
'dc.monto' ,
'dc.fecha as fecha_vencimiento' ,
DB :: raw ( 'DATEDIFF(CURDATE(), dc.fecha) as dias_mora' ),
])
-> get ();
Registrar pago de cuota Endpoint: PUT /api/cuentas-pagar/{id}/pagarpublic function pagarCuota ( $id )
{
$cuota = DiaCompra :: findOrFail ( $id );
$cuota -> update ([
'estado' => '0' , // Pagado
'fecha_pago' => now ()
]);
// Opcional: Registrar movimiento de caja/banco
MovimientoCaja :: create ([ ... ]);
return response () -> json ([
'success' => true ,
'message' => 'Cuota pagada exitosamente'
]);
}
Estados de cuotas: A diferencia de cuentas por cobrar (que usa P/C/V), las compras usan 1/0 para estado de cuotas.
Anulación de Compras
Proceso de anulación
Endpoint: POST /api/compras/{id}/anular
// CompraController.php línea 304-366
public function anular ( $id )
{
DB :: beginTransaction ();
try {
$compra = Compra :: where ( 'id_empresa' , $user -> id_empresa )
-> findOrFail ( $id );
if ( $compra -> estado == '0' ) {
return response () -> json ([
'success' => false ,
'message' => 'La compra ya está anulada'
], 400 );
}
// 1. Cambiar estado
$compra -> estado = '0' ;
$compra -> save ();
// 2. REVERTIR STOCK
foreach ( $compra -> detalles as $detalle ) {
$producto = Producto :: find ( $detalle -> id_producto );
if ( $producto ) {
$stockAnterior = $producto -> cantidad ;
$stockNuevo = max ( 0 , $stockAnterior - $detalle -> cantidad );
$producto -> cantidad = $stockNuevo ;
$producto -> save ();
// Registrar movimiento
MovimientoStock :: create ([
'id_producto' => $detalle -> id_producto ,
'tipo_movimiento' => 'salida' ,
'cantidad' => $detalle -> cantidad ,
'stock_anterior' => $stockAnterior ,
'stock_nuevo' => $stockNuevo ,
'tipo_documento' => 'anulacion_compra' ,
'id_documento' => $compra -> id_compra ,
'documento_referencia' => $compra -> serie . '-' . $compra -> numero ,
'motivo' => 'Anulación de compra' ,
'id_almacen' => 1 ,
'id_empresa' => $compra -> id_empresa ,
'id_usuario' => $user -> id ,
'fecha_movimiento' => now ()
]);
}
}
DB :: commit ();
return response () -> json ([
'success' => true ,
'message' => 'Compra anulada exitosamente'
]);
} catch ( Exception $e ) {
DB :: rollBack ();
return response () -> json ([
'success' => false ,
'message' => 'Error al anular compra: ' . $e -> getMessage ()
], 500 );
}
}
La anulación DISMINUYE el stock automáticamente. Usar solo si los productos no se recibieron o fueron devueltos al proveedor.
Movimientos de Stock
Entrada por compra
INSERT INTO movimientos_stock (
id_producto,
tipo_movimiento, -- 'entrada'
cantidad,
stock_anterior,
stock_nuevo,
tipo_documento, -- 'compra'
id_documento, -- id_compra
documento_referencia, -- 'F001-00001234'
motivo, -- 'Compra a proveedor'
id_almacen, -- 1
fecha_movimiento
);
Salida por anulación
INSERT INTO movimientos_stock (
tipo_movimiento, -- 'salida'
tipo_documento, -- 'anulacion_compra'
motivo, -- 'Anulación de compra'
...
);
Reportes y Exportación
Reporte de Compras
Ruta: /guias/reportes-compras
Filtros disponibles:
Rango de fechas
Proveedor
Estado (Activo/Anulado)
Moneda
Exportar a Excel
Endpoint: GET /api/compras/export/excel?fecha_inicio=...&fecha_fin=...
Columnas incluidas:
Fecha emisión
Proveedor (RUC y razón social)
Documento (serie-número)
Tipo de pago
Moneda
Total
Estado
Implementación:
use PhpOffice\PhpSpreadsheet\ Spreadsheet ;
use PhpOffice\PhpSpreadsheet\Writer\ Xlsx ;
public function exportExcel ( Request $request )
{
$compras = Compra :: with ( 'proveedor' )
-> whereBetween ( 'fecha_emision' , [ $inicio , $fin ])
-> get ();
$spreadsheet = new Spreadsheet ();
$sheet = $spreadsheet -> getActiveSheet ();
// Headers
$sheet -> setCellValue ( 'A1' , 'Fecha' );
$sheet -> setCellValue ( 'B1' , 'Proveedor' );
$sheet -> setCellValue ( 'C1' , 'Documento' );
$sheet -> setCellValue ( 'D1' , 'Total' );
// Data
$row = 2 ;
foreach ( $compras as $compra ) {
$sheet -> setCellValue ( 'A' . $row , $compra -> fecha_emision );
$sheet -> setCellValue ( 'B' . $row , $compra -> proveedor -> razon_social );
$sheet -> setCellValue ( 'C' . $row , $compra -> serie . '-' . $compra -> numero );
$sheet -> setCellValue ( 'D' . $row , $compra -> total );
$row ++ ;
}
$writer = new Xlsx ( $spreadsheet );
$filename = 'compras_' . date ( 'YmdHis' ) . '.xlsx' ;
return response () -> streamDownload ( function () use ( $writer ) {
$writer -> save ( 'php://output' );
}, $filename );
}
Errores Comunes
Error: Ya existe una compra registrada con este documento
Causa: Se intenta registrar la misma factura del proveedor dos veces.Solución:
Verificar serie y número del documento
Buscar la compra existente en la lista
Si fue anulada incorrectamente, contactar soporte
Validación: CompraController.php:114-126
Stock no aumentó después de guardar
Causa: Error en la transacción o producto no encontrado.Solución:
Verificar en módulo Inventario el stock actual
Revisar tabla movimientos_stock para confirmar entrada
Si no hay movimiento, la compra se guardó mal → Anular y recrear
Query diagnóstico: SELECT * FROM movimientos_stock
WHERE tipo_documento = 'compra'
AND id_documento = {compra_id};
Cuotas no aparecen en Cuentas por Pagar
Causa: Compra registrada como Contado en lugar de Crédito.Solución:
No se puede cambiar tipo de pago después de guardar
Anular compra y volver a crear con Crédito
Definir cuotas antes de guardar
Costo de producto no se actualizó
Causa: El sistema actualiza costo solo del primer producto en la compra.Explicación: // CompraController.php línea 176
$producto -> costo = $prod [ 'costo' ];
Si el mismo producto se compró antes a otro precio, el costo se sobrescribe con el último. Recomendación: Usar costo promedio ponderado (requiere modificación del código).
Integraciones
Con módulo de Productos
Actualiza productos.cantidad automáticamente
Actualiza productos.costo con último precio de compra
Registra en movimientos_stock para trazabilidad
Con módulo de Finanzas
Cuentas por Pagar muestra cuotas pendientes de tabla dias_compras
Al pagar cuota, puede registrar movimiento de caja/banco
Reportes de flujo de efectivo incluyen compras
Con módulo de Proveedores
Historial de compras por proveedor
Estadísticas: total comprado, deuda pendiente
Evaluación de proveedores por plazo y cumplimiento
Referencias Técnicas
Controlador: app/Http/Controllers/CompraController.php
Modelos:
app/Models/Compra.php
app/Models/ProductoCompra.php
app/Models/DiaCompra.php
app/Models/Proveedor.php
Frontend:
resources/js/components/Compras/CompraForm.jsx
resources/js/components/Compras/hooks/useCompraForm.js
resources/js/components/shared/ProveedorAutocomplete.jsx
resources/js/components/shared/CompraSidebar.jsx
Rutas API:
GET / api / compras // Listar compras
POST / api / compras // Crear compra
GET / api / compras / { id } // Ver detalle
POST / api / compras / { id } / anular // Anular compra