Skip to main content

Módulo de Cotizaciones

El módulo de cotizaciones permite crear presupuestos formales para clientes, con posibilidad de convertirlos automáticamente en facturas o boletas. Soporta múltiples monedas, descuentos, cuotas de pago y productos personalizados.

Características Principales

Presupuestos Formales

Genera cotizaciones profesionales con logo y datos de la empresa

Conversión a Venta

Convierte cotizaciones aprobadas en facturas/boletas con un clic

Plan de Pagos

Define cuotas con fechas de vencimiento y montos personalizados

Clientes Libres

Crea cotizaciones sin cliente registrado usando nombre libre

Estados de Cotización

Cada cotización puede estar en uno de estos estados:
Recién creada, esperando respuesta del cliente.
Cliente aceptó la cotización. Se puede convertir a venta.
Cliente rechazó la cotización o se eliminó.
La fecha de validez expiró sin respuesta.

Flujo de Trabajo

1

Seleccionar Cliente

Elige un cliente existente o ingresa datos libres:
// CotizacionController.php:112-137
$idCliente = $request->id_cliente;
$clienteNombre = null;

if (!$idCliente && $request->cliente_documento) {
    // Buscar o crear cliente por documento
    $clienteModel = Cliente::where('documento', $request->cliente_documento)
        ->where('id_empresa', $idEmpresa)
        ->first();

    if (!$clienteModel) {
        $clienteModel = Cliente::create([...]);
    }
    $idCliente = $clienteModel->id_cliente;
} elseif (!$idCliente) {
    // Cliente libre: guardar solo nombre
    $clienteNombre = $request->cliente_nombre;
}
2

Agregar Productos

Selecciona productos del catálogo:
  • Precio unitario: Precio del catálogo
  • Precio especial: Descuento personalizado para esta cotización
  • Cantidad: Unidades a cotizar
{
  "productos": [
    {
      "producto_id": 15,
      "codigo": "PROD-001",
      "nombre": "Laptop HP",
      "cantidad": 5,
      "precio_unitario": 2500.00,
      "precio_especial": 2200.00
    }
  ]
}
3

Configurar Precios

Define moneda, descuento e IGV:
  • Moneda: PEN o USD
  • Tipo de cambio: Solo si es USD
  • Descuento global: Monto a descontar del total
  • Aplicar IGV: Si incluye impuesto (18%)
// CotizacionController.php:145-161
$montoBruto = 0;
foreach ($request->productos as $prod) {
    $precio = $prod['precio_especial'] ?? $prod['precio_unitario'];
    $montoBruto += $precio * $prod['cantidad'];
}

$descuento = $request->descuento ?? 0;
$total = $montoBruto - $descuento;

if ($request->aplicar_igv) {
    $subtotal = $total / 1.18;
    $igv = $total - $subtotal;
}
4

Definir Plan de Pagos

Opcionalmente, crea cuotas:
{
  "cuotas": [
    {
      "tipo": "inicial",
      "monto": 5000.00,
      "fecha_vencimiento": "2026-03-15"
    },
    {
      "tipo": "cuota",
      "monto": 5000.00,
      "fecha_vencimiento": "2026-04-15"
    }
  ]
}
5

Generar Cotización

El sistema asigna un número correlativo y crea el documento:
// CotizacionController.php:140-144
$ultimaCotizacion = Cotizacion::where('id_empresa', $idEmpresa)
    ->orderBy('numero', 'desc')
    ->first();
$numero = $ultimaCotizacion ? $ultimaCotizacion->numero + 1 : 1;

Cálculo de Totales

El sistema usa precios con IGV incluido:
Importante: Los precios de venta en Perú suelen incluir IGV. El sistema calcula el subtotal dividiendo el total entre 1.18.
// CotizacionController.php:145-161
$montoBruto = 0;
foreach ($request->productos as $prod) {
    $precio = $prod['precio_especial'] ?? $prod['precio_unitario'];
    $montoBruto += $precio * $prod['cantidad'];
}

$descuento = $request->descuento ?? 0;
$total = $montoBruto - $descuento;  // Total con IGV incluido

$igv = 0;
$subtotal = $total;  // Base imponible

if ($request->aplicar_igv) {
    $subtotal = $total / 1.18;  // Operaciones Gravadas
    $igv = $total - $subtotal;   // IGV = 18% del subtotal
}

Implementación Backend

Crear Cotización

// app/Http/Controllers/CotizacionController.php:68-234

public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'fecha' => 'required|date',
        'id_cliente' => 'nullable|exists:clientes,id_cliente',
        'cliente_documento' => 'nullable|string|max:11',
        'cliente_nombre' => 'nullable|string|max:255',
        'moneda' => 'required|in:PEN,USD',
        'tipo_cambio' => 'nullable|numeric',
        'aplicar_igv' => 'required|boolean',
        'descuento' => 'nullable|numeric|min:0',
        'productos' => 'required|array|min:1',
        'productos.*.producto_id' => 'required|exists:productos,id_producto',
        'productos.*.cantidad' => 'required|numeric|min:0.01',
        'productos.*.precio_unitario' => 'required|numeric|min:0',
        'cuotas' => 'nullable|array',
    ]);

    if ($validator->fails()) {
        return response()->json([
            'success' => false,
            'errors' => $validator->errors()
        ], 422);
    }

    DB::beginTransaction();

    try {
        $user = $request->user();
        $idEmpresa = $user->id_empresa;

        // Resolver cliente
        $idCliente = $request->id_cliente;
        $clienteNombre = null;

        if (!$idCliente && $request->cliente_documento) {
            $clienteModel = Cliente::where('documento', $request->cliente_documento)
                ->where('id_empresa', $idEmpresa)
                ->first();

            if (!$clienteModel) {
                $clienteModel = Cliente::create([...]);
            }
            $idCliente = $clienteModel->id_cliente;
        } elseif (!$idCliente) {
            $clienteNombre = $request->cliente_nombre ?: $request->cliente_datos;
        }

        // Generar número correlativo
        $ultimaCotizacion = Cotizacion::where('id_empresa', $idEmpresa)
            ->orderBy('numero', 'desc')
            ->first();
        $numero = $ultimaCotizacion ? $ultimaCotizacion->numero + 1 : 1;

        // Calcular totales
        $montoBruto = 0;
        foreach ($request->productos as $prod) {
            $precio = $prod['precio_especial'] ?? $prod['precio_unitario'];
            $montoBruto += $precio * $prod['cantidad'];
        }

        $descuento = $request->descuento ?? 0;
        $total = $montoBruto - $descuento;
        
        $igv = 0;
        $subtotal = $total;

        if ($request->aplicar_igv) {
            $subtotal = $total / 1.18;
            $igv = $total - $subtotal;
        }

        // Crear cotización
        $cotizacion = Cotizacion::create([
            'numero' => $numero,
            'fecha' => $request->fecha,
            'id_cliente' => $idCliente,
            'cliente_nombre' => $clienteNombre,
            'direccion' => $request->direccion,
            'subtotal' => $subtotal,
            'igv' => $igv,
            'total' => $total,
            'descuento' => $descuento,
            'aplicar_igv' => $request->aplicar_igv,
            'moneda' => $request->moneda,
            'tipo_cambio' => $request->tipo_cambio,
            'dias_pago' => $request->dias_pago,
            'asunto' => $request->asunto,
            'observaciones' => $request->observaciones,
            'estado' => 'pendiente',
            'id_empresa' => $idEmpresa,
            'id_usuario' => $user->id,
        ]);

        // Crear detalles
        foreach ($request->productos as $prod) {
            $precio = $prod['precio_especial'] ?? $prod['precio_unitario'];
            $subtotalDetalle = $precio * $prod['cantidad'];

            CotizacionDetalle::create([
                'cotizacion_id' => $cotizacion->id,
                'producto_id' => $prod['producto_id'],
                'codigo' => $prod['codigo'] ?? null,
                'nombre' => $prod['nombre'],
                'descripcion' => $prod['descripcion'] ?? null,
                'cantidad' => $prod['cantidad'],
                'precio_unitario' => $prod['precio_unitario'],
                'precio_especial' => $prod['precio_especial'] ?? null,
                'subtotal' => $subtotalDetalle,
            ]);
        }

        // Crear cuotas
        if ($request->has('cuotas') && is_array($request->cuotas)) {
            foreach ($request->cuotas as $index => $cuota) {
                CotizacionCuota::create([
                    'cotizacion_id' => $cotizacion->id,
                    'numero_cuota' => $index + 1,
                    'monto' => $cuota['monto'],
                    'fecha_vencimiento' => $cuota['fecha_vencimiento'],
                    'tipo' => $cuota['tipo'] ?? 'cuota',
                ]);
            }
        }

        DB::commit();

        return response()->json([
            'success' => true,
            'message' => 'Cotización creada exitosamente',
            'data' => $cotizacion->load(['cliente', 'detalles', 'cuotas'])
        ], 201);

    } catch (\Exception $e) {
        DB::rollBack();
        return response()->json([
            'success' => false,
            'message' => 'Error: ' . $e->getMessage()
        ], 500);
    }
}

Actualizar Cotización

La actualización elimina y recrea detalles y cuotas:
// CotizacionController.php:343-346
$cotizacion->detalles()->delete();
$cotizacion->cuotas()->delete();

// Crear nuevos detalles
foreach ($request->productos as $prod) {
    CotizacionDetalle::create([...]);
}

// Crear nuevas cuotas
if ($request->has('cuotas')) {
    foreach ($request->cuotas as $index => $cuota) {
        CotizacionCuota::create([...]);
    }
}

Conversión a Venta

Cuando una cotización se aprueba, se marca como “aprobada”:
// VentasController.php:336-341 (al crear venta desde cotización)
if (!empty($validated['cotizacion_id'])) {
    Cotizacion::where('id', $validated['cotizacion_id'])
        ->where('id_empresa', $user->id_empresa)
        ->update(['estado' => 'aprobada']);
}
La conversión NO es automática. El usuario debe crear manualmente una venta y vincularla con cotizacion_id en el request.

Cambiar Estado

Endpoint para modificar el estado de una cotización:
// CotizacionController.php:424-454
public function cambiarEstado(Request $request, $id)
{
    $validator = Validator::make($request->all(), [
        'estado' => 'required|in:pendiente,aprobada,rechazada,vencida',
    ]);

    if ($validator->fails()) {
        return response()->json([
            'success' => false,
            'errors' => $validator->errors()
        ], 422);
    }

    $user = $request->user();
    $cotizacion = Cotizacion::where('id_empresa', $user->id_empresa)
        ->findOrFail($id);
        
    $cotizacion->update(['estado' => $request->estado]);

    return response()->json([
        'success' => true,
        'message' => 'Estado actualizado',
        'data' => $cotizacion
    ]);
}

Número de Cotización

El formato es COT-XXXXXX:
// CotizacionController.php:459-480
public function proximoNumero(Request $request)
{
    $user = $request->user();

    $ultimaCotizacion = Cotizacion::where('id_empresa', $user->id_empresa)
        ->orderBy('numero', 'desc')
        ->first();

    $proximoNumero = $ultimaCotizacion ? $ultimaCotizacion->numero + 1 : 1;

    return response()->json([
        'success' => true,
        'numero' => 'COT-' . str_pad($proximoNumero, 6, '0', STR_PAD_LEFT)
    ]);
}

Exportar PDF

Las cotizaciones se pueden exportar a PDF usando mPDF:
// app/Http/Controllers/Reportes/CotizacionPdfController.php

public function generar($id)
{
    $cotizacion = Cotizacion::with([
        'cliente', 'empresa', 'detalles.producto', 'cuotas'
    ])->findOrFail($id);

    $mpdf = new \Mpdf\Mpdf(['format' => 'A4']);
    $html = view('reportes.cotizacion', ['cotizacion' => $cotizacion])->render();
    $mpdf->WriteHTML($html);
    
    return $mpdf->Output('COT-' . $cotizacion->numero . '.pdf', 'I');
}

Vista de Listado

El sistema usa una vista SQL optimizada:
// CotizacionController.php:19-39
public function index(Request $request)
{
    $user = $request->user();
    $idEmpresa = $user->id_empresa;
    
    // Usa view_cotizaciones para mejor performance
    $cotizaciones = DB::table('view_cotizaciones')
        ->where('id_empresa', $idEmpresa)
        ->orderBy('id', 'desc')
        ->get();
    
    return response()->json([
        'success' => true,
        'data' => $cotizaciones
    ]);
}

Tablas de Base de Datos

TablaDescripciónCampos Clave
cotizacionesEncabezado de la cotizaciónid, numero, id_cliente, cliente_nombre, total, estado
detalle_cotizacionesProductos cotizadoscotizacion_id, producto_id, cantidad, precio_unitario, precio_especial
cuotas_cotizacionPlan de pagoscotizacion_id, numero_cuota, monto, fecha_vencimiento, tipo
view_cotizacionesVista SQL optimizadaJoin con cliente y usuario

Modelo: Cotizacion

// app/Models/Cotizacion.php

class Cotizacion extends Model
{
    protected $table = 'cotizaciones';

    protected $fillable = [
        'numero', 'fecha', 'id_cliente', 'cliente_nombre',
        'direccion', 'subtotal', 'igv', 'total', 'descuento',
        'aplicar_igv', 'moneda', 'tipo_cambio', 'dias_pago',
        'asunto', 'observaciones', 'estado', 'id_empresa', 'id_usuario'
    ];

    public function cliente()
    {
        return $this->belongsTo(Cliente::class, 'id_cliente');
    }

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

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

Endpoints API

Listar Cotizaciones

GET /api/cotizaciones

Ver Detalle

GET /api/cotizaciones/:id

Crear Cotización

POST /api/cotizaciones

Actualizar

PUT /api/cotizaciones/:id

Cambiar Estado

POST /api/cotizaciones/:id/estado

Eliminar

DELETE /api/cotizaciones/:id

Buenas Prácticas

Recomendación: Define una fecha de validez en las “Observaciones” para indicar cuánto tiempo es válida la cotización (ej: “Válido por 15 días”).
Las cotizaciones NO afectan el stock ni generan movimientos contables. Solo son documentos informativos.
Cuando conviertas una cotización a venta, verifica que los productos aún tengan stock disponible.

Próximos Pasos

Convertir a Venta

Aprende cómo crear ventas desde cotizaciones

Facturación

Sistema de ventas y comprobantes

Plan de Pagos

Gestión de cuotas y cobranzas

Reportes

Exporta cotizaciones a PDF y Excel

Build docs developers (and LLMs) love