Skip to main content

Módulo de Compras

El módulo de compras permite registrar las adquisiciones a proveedores, generando automáticamente aumentos de stock, cuentas por pagar y movimientos contables. No se envía a SUNAT, solo se registra localmente.

Características Principales

Registro de Compras

Registra facturas y boletas recibidas de proveedores

Aumento de Stock

Incrementa automáticamente el inventario

Cuentas por Pagar

Genera obligaciones de pago con cronograma de cuotas

Múltiples Empresas

Asigna compras a varias empresas del grupo

Tipos de Pago

Pago inmediato al proveedor. No genera cuotas.
Pago diferido con plan de cuotas configurables.

Flujo de Trabajo

1

Seleccionar Proveedor

Busca el proveedor por RUC o crea uno nuevo:
// Validación
'id_proveedor' => 'required|exists:proveedores,proveedor_id'
2

Datos del Documento

Ingresa los datos del comprobante recibido:
  • Tipo de documento: Factura, boleta, recibo, etc.
  • Serie: Ej. F001, B002
  • Número: Correlativo del proveedor
  • Fecha de emisión
  • Fecha de vencimiento (solo para crédito)
El sistema valida que no exista duplicado:
// CompraController.php:113-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([
        'message' => 'Ya existe una compra con este documento'
    ], 400);
}
3

Agregar Productos

Selecciona los productos comprados:
{
  "productos": [
    {
      "id_producto": 42,
      "cantidad": 100,
      "costo": 45.50
    },
    {
      "id_producto": 58,
      "cantidad": 50,
      "costo": 28.00
    }
  ]
}
4

Configurar Pago

Define tipo de pago y moneda:
  • Tipo: Contado (1) o Crédito (2)
  • Moneda: PEN o USD
  • Si es crédito, define cuotas:
{
  "id_tipo_pago": 2,
  "moneda": "PEN",
  "cuotas": [
    {
      "monto": 2500.00,
      "fecha": "2026-04-15"
    },
    {
      "monto": 2500.00,
      "fecha": "2026-05-15"
    }
  ]
}
5

Asignar Empresas

Selecciona qué empresas del grupo compartirán esta compra:
// CompraController.php:198-200
if (!empty($request->empresas_ids)) {
    $compra->empresas()->attach($request->empresas_ids);
}
6

Registrar Compra

El sistema automáticamente:
  • Crea el registro de la compra
  • Incrementa el stock de cada producto
  • Actualiza el costo del producto
  • Registra movimientos de stock
  • Genera cuotas de pago (si es crédito)

Implementación Backend

Crear Compra

// app/Http/Controllers/CompraController.php:70-232

public function store(Request $request)
{
    DB::beginTransaction();
    
    try {
        $user = Auth::user();
        $idEmpresa = $user->id_empresa;

        // 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',
            'fecha_emision' => 'required|date',
            'fecha_vencimiento' => 'nullable|date',
            'id_tipo_pago' => 'required|in:1,2',
            'moneda' => 'required|in:PEN,USD',
            'productos' => 'required|array|min:1',
            'productos.*.id_producto' => 'required|exists:productos,id_producto',
            'productos.*.cantidad' => 'required|numeric|min:0.01',
            'productos.*.costo' => 'required|numeric|min:0',
        ]);

        // Validar formato de serie y número
        if (!preg_match('/^[A-Z0-9]{1,4}$/', $request->serie)) {
            return response()->json([
                'message' => 'Serie inválida. Use 1-4 caracteres alfanuméricos'
            ], 400);
        }

        if (!preg_match('/^[0-9]{1,8}$/', $request->numero)) {
            return response()->json([
                'message' => 'Número inválido. Use solo dígitos (máximo 8)'
            ], 400);
        }

        // Validar documento duplicado
        $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([
                'message' => 'Ya existe una compra con este documento del proveedor'
            ], 400);
        }

        // Calcular totales
        $subtotal = 0;
        foreach ($request->productos as $prod) {
            $subtotal += $prod['cantidad'] * $prod['costo'];
        }
        
        $igv = 0;  // Las compras normalmente no llevan IGV
        $total = $subtotal + $igv;

        // Crear compra
        $compra = Compra::create([
            'id_tido' => $request->tipo_doc,
            'serie' => $request->serie,
            'numero' => $request->numero,
            'id_proveedor' => $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,
            'direccion' => $request->direccion ?? '',
            'observaciones' => $request->observaciones ?? '',
            'id_empresa' => $idEmpresa,
            'id_usuario' => $user->id,
            'estado' => '1'  // Activo
        ]);

        // 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']
            ]);

            // Actualizar stock del producto
            $producto = Producto::find($prod['id_producto']);
            $stockAnterior = $producto->cantidad;
            $stockNuevo = $stockAnterior + $prod['cantidad'];
            
            $producto->cantidad = $stockNuevo;
            $producto->costo = $prod['costo'];  // Actualizar costo
            $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' => $user->id,
                'fecha_movimiento' => now()
            ]);
        }

        // Guardar empresas asociadas
        if (!empty($request->empresas_ids)) {
            $compra->empresas()->attach($request->empresas_ids);
        }

        // Si es crédito, guardar cuotas
        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'  // Pendiente
                ]);
            }
        }

        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: ' . $e->getMessage()
        ], 500);
    }
}

Actualización de Stock

Cada producto comprado incrementa su stock:
// CompraController.php:171-195
$producto = Producto::find($prod['id_producto']);
$stockAnterior = $producto->cantidad;
$stockNuevo = $stockAnterior + $prod['cantidad'];

// Actualizar cantidad y costo
$producto->cantidad = $stockNuevo;
$producto->costo = $prod['costo'];
$producto->save();

// Registrar movimiento con trazabilidad
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,
    'fecha_movimiento' => now()
]);
Importante: El costo del producto se actualiza con el último precio de compra. Esto afecta el cálculo de utilidades.

Anulación de Compras

Al anular una compra, el stock se revierte:
// CompraController.php:304-366
public function anular($id)
{
    DB::beginTransaction();
    
    try {
        $user = Auth::user();
        $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);
        }

        // Anular compra
        $compra->estado = '0';
        $compra->save();

        // 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 de salida
                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: ' . $e->getMessage()
        ], 500);
    }
}
Atención: Si anulas una compra después de vender los productos, el stock puede quedar negativo. Verifica antes de anular.

Listado de Compras

// CompraController.php:22-65
public function index(Request $request)
{
    $user = Auth::user();
    $idEmpresa = $user->id_empresa;

    $compras = Compra::with(['proveedor', 'usuario'])
        ->where('id_empresa', $idEmpresa)
        ->orderBy('id_compra', 'desc')
        ->get()
        ->map(function ($compra) {
            return [
                'id_compra' => $compra->id_compra,
                'documento' => $compra->serie . '-' . str_pad($compra->numero, 8, '0', STR_PAD_LEFT),
                'serie' => $compra->serie,
                'numero' => $compra->numero,
                'fecha_emision' => $compra->fecha_emision->format('Y-m-d'),
                'fecha_vencimiento' => $compra->fecha_vencimiento 
                    ? $compra->fecha_vencimiento->format('Y-m-d') 
                    : null,
                'proveedor' => [
                    'proveedor_id' => $compra->proveedor->proveedor_id ?? null,
                    'ruc' => $compra->proveedor->ruc ?? '',
                    'razon_social' => $compra->proveedor->razon_social ?? 'Sin proveedor',
                ],
                'tipo_pago' => $compra->id_tipo_pago == 1 ? 'Contado' : 'Crédito',
                'id_tipo_pago' => $compra->id_tipo_pago,
                'moneda' => $compra->moneda,
                'total' => number_format($compra->total, 2, '.', ''),
                'estado' => $compra->estado,
                'estado_nombre' => $compra->estado == '1' ? 'Activo' : 'Anulado',
                'usuario' => $compra->usuario->name ?? 'Sistema',
            ];
        });

    return response()->json([
        'success' => true,
        'data' => $compras
    ]);
}

Ver Detalle

Incluye productos, cuotas y empresas asociadas:
// CompraController.php:238-299
public function show($id)
{
    $user = Auth::user();
    $compra = Compra::with([
        'proveedor', 'detalles.producto', 'cuotas', 'usuario', 'empresas'
    ])
        ->where('id_empresa', $user->id_empresa)
        ->findOrFail($id);

    return response()->json([
        'success' => true,
        'data' => [
            'id_compra' => $compra->id_compra,
            'serie' => $compra->serie,
            'numero' => $compra->numero,
            'fecha_emision' => $compra->fecha_emision->format('Y-m-d'),
            'fecha_vencimiento' => $compra->fecha_vencimiento 
                ? $compra->fecha_vencimiento->format('Y-m-d') 
                : null,
            'id_tipo_pago' => $compra->id_tipo_pago,
            'moneda' => $compra->moneda,
            'subtotal' => $compra->subtotal,
            'igv' => $compra->igv,
            'total' => $compra->total,
            'proveedor' => [
                'proveedor_id' => $compra->proveedor->proveedor_id,
                'ruc' => $compra->proveedor->ruc,
                'razon_social' => $compra->proveedor->razon_social,
                'direccion' => $compra->proveedor->direccion,
            ],
            'detalles' => $compra->detalles->map(function ($detalle) {
                return [
                    'id_producto' => $detalle->id_producto,
                    'codigo' => $detalle->producto->codigo ?? '',
                    'nombre' => $detalle->producto->nombre ?? '',
                    'cantidad' => $detalle->cantidad,
                    'costo' => $detalle->costo,
                    'subtotal' => $detalle->subtotal,
                ];
            }),
            'cuotas' => $compra->cuotas->map(function ($cuota) {
                return [
                    'monto' => $cuota->monto,
                    'fecha' => $cuota->fecha->format('Y-m-d'),
                    'estado' => $cuota->estado,
                ];
            }),
            'empresas' => $compra->empresas->map(function ($emp) {
                return [
                    'id_empresa' => $emp->id_empresa,
                    'comercial' => $emp->comercial,
                    'ruc' => $emp->ruc,
                ];
            }),
        ]
    ]);
}

Modelos Relacionados

Modelo: Compra

// app/Models/Compra.php

class Compra extends Model
{
    protected $table = 'compras';
    protected $primaryKey = 'id_compra';

    protected $fillable = [
        'id_tido', 'serie', 'numero', 'id_proveedor',
        'fecha_emision', 'fecha_vencimiento', 'id_tipo_pago',
        'moneda', 'subtotal', 'igv', 'total',
        'direccion', 'observaciones', 'id_empresa', 'id_usuario', 'estado'
    ];

    protected $casts = [
        'fecha_emision' => 'date',
        'fecha_vencimiento' => 'date',
    ];

    public function proveedor()
    {
        return $this->belongsTo(Proveedor::class, 'id_proveedor', 'proveedor_id');
    }

    public function detalles()
    {
        return $this->hasMany(ProductoCompra::class, 'id_compra');
    }

    public function cuotas()
    {
        return $this->hasMany(DiaCompra::class, 'id_compra');
    }

    public function empresas()
    {
        return $this->belongsToMany(Empresa::class, 'compra_empresa', 'id_compra', 'id_empresa');
    }
}

Tablas de Base de Datos

TablaDescripciónCampos Clave
comprasEncabezado de la compraid_compra, serie, numero, id_proveedor, total, estado
detalle_comprasProductos compradosid_compra, id_producto, cantidad, costo
dias_comprasCuotas de pagoid_compra, monto, fecha, estado (1=Pendiente, 0=Pagado)
compra_empresaRelación many-to-manyid_compra, id_empresa
movimientos_stockTrazabilidad de inventariotipo_documento='compra', id_documento=id_compra

Endpoints API

Listar Compras

GET /api/compras

Ver Detalle

GET /api/compras/:id

Crear Compra

POST /api/compras

Anular Compra

POST /api/compras/:id/anular

Exportación

Exporta compras a Excel:
// app/Http/Controllers/Exports/CompraExportController.php

public function exportar(Request $request)
{
    $compras = Compra::with(['proveedor'])
        ->where('id_empresa', $request->user()->id_empresa)
        ->whereBetween('fecha_emision', [$request->fecha_inicio, $request->fecha_fin])
        ->get();

    $spreadsheet = new Spreadsheet();
    $sheet = $spreadsheet->getActiveSheet();
    
    // Headers
    $sheet->setCellValue('A1', 'Fecha');
    $sheet->setCellValue('B1', 'Documento');
    $sheet->setCellValue('C1', 'Proveedor');
    $sheet->setCellValue('D1', 'Total');
    $sheet->setCellValue('E1', 'Estado');
    
    // Data
    $row = 2;
    foreach ($compras as $compra) {
        $sheet->setCellValue('A' . $row, $compra->fecha_emision->format('d/m/Y'));
        $sheet->setCellValue('B' . $row, $compra->serie . '-' . $compra->numero);
        $sheet->setCellValue('C' . $row, $compra->proveedor->razon_social);
        $sheet->setCellValue('D' . $row, $compra->total);
        $sheet->setCellValue('E' . $row, $compra->estado == '1' ? 'Activo' : 'Anulado');
        $row++;
    }
    
    $writer = new Xlsx($spreadsheet);
    // ...
}

Integración con Cuentas por Pagar

Las compras a crédito generan automáticamente cuentas por pagar:
1

Crear Cuotas

Al registrar una compra a crédito, se guardan las cuotas en dias_compras
2

Estado Pendiente

Cada cuota inicia con estado 1 (Pendiente)
3

Registro de Pago

Desde el módulo de Cuentas por Pagar, se marca la cuota como pagada (estado 0)
4

Trazabilidad

Se registra el movimiento bancario o de caja asociado al pago
Ver más detalles en Cuentas por Pagar.

Buenas Prácticas

Recomendación: Registra las compras apenas recibas el comprobante del proveedor para mantener el stock actualizado.
No confundas el número de serie de la compra con las series de emisión de SUNAT. El número/serie de compra es el que viene en el documento del proveedor.
Las compras NO se envían a SUNAT. Este módulo es solo para registro interno y control de stock.

Próximos Pasos

Proveedores

Gestión de proveedores

Inventario

Control de stock y movimientos

Cuentas por Pagar

Gestión de obligaciones con proveedores

Reportes

Exporta compras a Excel y PDF

Build docs developers (and LLMs) love