Skip to main content

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

1

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
2

Seleccionar Proveedor

Componente: ProveedorAutocomplete.jsx

Bú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:
  1. Click en ”+ Nuevo Proveedor”
  2. Ingresar RUC (consulta automática API Perú)
  3. Completar datos: razón social, dirección, contacto
  4. Guardar → Proveedor se crea y selecciona automáticamente
Los proveedores se guardan en tabla proveedores con scope de id_empresa.
3

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_sunat

Serie 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.
4

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
  1. Click “Búsqueda Múltiple”
  2. Se abre modal con tabla de productos
  3. Filtros disponibles:
    • Buscar por código/nombre
    • Filtrar por categoría
    • Ordenar por stock/precio
  4. Seleccionar múltiples productos (checkboxes)
  5. Click “Agregar Seleccionados”
  6. 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);
};
5

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:
  1. Fecha de vencimiento
  2. 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
        ]);
    }
}
6

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);
    }
}
7

Impresión y Confirmación

Después de guardar exitosamente, aparece modal PrintOptionsModal:

Opciones disponibles:

  1. Ver PDF de Compra
    • Ruta: /reporteCompra/a4.php?id={compra_id}
    • Formato A4 con detalles completos
  2. Ver Lista de Compras
    • Redirección a /compras
  3. 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
8

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}/pagar
public 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:

  • 1 = Pendiente
  • 0 = Pagado
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

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
Causa: Error en la transacción o producto no encontrado.Solución:
  1. Verificar en módulo Inventario el stock actual
  2. Revisar tabla movimientos_stock para confirmar entrada
  3. 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};
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
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

Build docs developers (and LLMs) love