up
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Models\Caja;
|
||||
use App\Models\Cliente;
|
||||
use App\Models\MovimientoCaja;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Forms;
|
||||
use Filament\Tables;
|
||||
use App\Models\Producto;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\Venta;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class PuntoVenta extends Page
|
||||
{
|
||||
protected static ?string $navigationGroup = 'Operación';
|
||||
protected static ?string $navigationIcon = 'heroicon-o-shopping-cart';
|
||||
protected static string $view = 'filament.pages.punto-venta';
|
||||
|
||||
public $barcodeBusqueda = '';
|
||||
|
||||
public $productos = [];
|
||||
public $carrito = [];
|
||||
public $total = 0;
|
||||
public $tipo_pago = 'Efectivo';
|
||||
|
||||
public $numeroDocumentoCliente = '';
|
||||
public $datosCliente = [
|
||||
'nombre' => null,
|
||||
'correo' => null,
|
||||
'telefono' => null,
|
||||
];
|
||||
public $cliente_id = null;
|
||||
|
||||
public $correoCotizacion = '';
|
||||
public $bodega_id = null; // Bodega seleccionada para la venta
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->productos = Producto::where('estado', true)
|
||||
->where(function ($query) {
|
||||
$query->where('stock', '>', 0)
|
||||
->orWhereHas('bodegas', function ($q) {
|
||||
$q->where('producto_bodega.stock', '>', 0);
|
||||
})
|
||||
->orWhereHas('variants', function ($q) {
|
||||
$q->where('stock', '>', 0)
|
||||
->orWhereHas('bodegas', function ($bq) {
|
||||
$bq->where('variante_bodega.stock', '>', 0);
|
||||
});
|
||||
});
|
||||
})
|
||||
->with('variants.color', 'variants.size', 'bodegas')
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
// Seleccionar bodega "Principal" por defecto
|
||||
$bodegaPrincipal = \App\Models\Bodega::where('nombre', 'Principal')->first();
|
||||
$this->bodega_id = $bodegaPrincipal ? $bodegaPrincipal->id : null;
|
||||
}
|
||||
|
||||
public function agregarAlCarrito($productoId, $tipo)
|
||||
{
|
||||
if ($tipo == 'p') {
|
||||
$producto = Producto::find($productoId);
|
||||
if (!$producto)
|
||||
return;
|
||||
|
||||
$key = 'p-' . $producto->id;
|
||||
|
||||
if (!isset($this->carrito[$key])) {
|
||||
$this->carrito[$key] = [
|
||||
'id' => $producto->id,
|
||||
'nombre' => $producto->nombre,
|
||||
'precio_venta' => $producto->precio_venta,
|
||||
'precio_original' => $producto->precio_venta,
|
||||
'precio_modificado' => $producto->precio_venta,
|
||||
'cantidad' => 1,
|
||||
'tipo' => 'p',
|
||||
];
|
||||
} else {
|
||||
$this->carrito[$key]['cantidad']++;
|
||||
}
|
||||
} else {
|
||||
$producto = ProductVariant::find($productoId);
|
||||
if (!$producto)
|
||||
return;
|
||||
|
||||
$key = 'v-' . $producto->id;
|
||||
|
||||
if (!isset($this->carrito[$key])) {
|
||||
$this->carrito[$key] = [
|
||||
'id' => $producto->producto->id,
|
||||
'nombre' => $producto->producto->nombre,
|
||||
'precio_venta' => $producto->producto->precio_venta,
|
||||
'precio_original' => $producto->producto->precio_venta,
|
||||
'precio_modificado' => $producto->producto->precio_venta,
|
||||
'variante' => 'Color: ' . $producto->color->name . ', Talla: ' . $producto->size->name,
|
||||
'variante_id' => $producto->id,
|
||||
'cantidad' => 1,
|
||||
'tipo' => 'v',
|
||||
];
|
||||
} else {
|
||||
$this->carrito[$key]['cantidad']++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->calcularTotal();
|
||||
}
|
||||
|
||||
|
||||
public function calcularTotal()
|
||||
{
|
||||
$this->total = collect($this->carrito)->sum(fn($item) => $item['precio_modificado'] * $item['cantidad']);
|
||||
}
|
||||
|
||||
public function actualizarPrecio($key, $nuevoPrecio)
|
||||
{
|
||||
if (isset($this->carrito[$key])) {
|
||||
// Validar que el precio sea válido (mayor o igual a 0)
|
||||
$precio = floatval($nuevoPrecio);
|
||||
if ($precio >= 0) {
|
||||
$this->carrito[$key]['precio_modificado'] = $precio;
|
||||
$this->calcularTotal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function procesarVenta()
|
||||
{
|
||||
if (empty($this->carrito)) {
|
||||
session()->flash('error', 'No se puede procesar la venta: el carrito está vacío.');
|
||||
return;
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$clienteId = null;
|
||||
|
||||
if ($this->numeroDocumentoCliente) {
|
||||
$cliente = Cliente::where('numero_documento', $this->numeroDocumentoCliente)->first();
|
||||
|
||||
if (!$cliente) {
|
||||
$datosCliente = [
|
||||
'nombre' => $this->datosCliente['nombre'] ?? "Cliente General",
|
||||
'correo' => $this->datosCliente['correo'] ?? null,
|
||||
'telefono' => $this->datosCliente['telefono'] ?? null,
|
||||
'numero_documento' => $this->numeroDocumentoCliente,
|
||||
'tipo_documento' => $this->datosCliente['tipo_documento'] ?? 'CC',
|
||||
];
|
||||
|
||||
$cliente = Cliente::create($datosCliente);
|
||||
}
|
||||
|
||||
$clienteId = $cliente->id;
|
||||
} elseif ($this->cliente_id) {
|
||||
$cliente = Cliente::find($this->cliente_id);
|
||||
if ($cliente) {
|
||||
$clienteId = $cliente->id;
|
||||
} else {
|
||||
$this->cliente_id = null;
|
||||
$clienteId = null;
|
||||
}
|
||||
}
|
||||
|
||||
$venta = Venta::create([
|
||||
'cliente_id' => $clienteId,
|
||||
'total' => $this->total,
|
||||
'tipo_pago' => $this->tipo_pago,
|
||||
'estado' => 'Pagado',
|
||||
]);
|
||||
|
||||
foreach ($this->carrito as $item) {
|
||||
$precioOriginal = $item['precio_original'];
|
||||
$precioModificado = $item['precio_modificado'];
|
||||
$descuentoAplicado = $precioOriginal - $precioModificado;
|
||||
$cantidadPendiente = $item['cantidad'];
|
||||
|
||||
// Identificar el producto o variante real
|
||||
$producto = null;
|
||||
$variante = null;
|
||||
|
||||
if (isset($item['variante_id'])) {
|
||||
$variante = ProductVariant::find($item['variante_id']);
|
||||
$producto = $variante->producto;
|
||||
} else {
|
||||
$producto = Producto::find($item['id']);
|
||||
}
|
||||
|
||||
// Estrategia de deducción de stock:
|
||||
// 1. Buscar en Bodega Principal
|
||||
// 2. Buscar en otras bodegas con stock
|
||||
// 3. Si falta, descontar de stock directo (o dejar negativo en Principal si se prefiere, aquí usaremos Principal como fallback)
|
||||
|
||||
$bodegas = \App\Models\Bodega::orderByRaw("CASE WHEN nombre = 'Principal' THEN 0 ELSE 1 END")->get();
|
||||
|
||||
foreach ($bodegas as $bodega) {
|
||||
if ($cantidadPendiente <= 0) break;
|
||||
|
||||
$stockDisponible = 0;
|
||||
|
||||
if ($variante) {
|
||||
$bodegaVariante = $variante->bodegas()->where('bodega_id', $bodega->id)->first();
|
||||
$stockDisponible = $bodegaVariante ? $bodegaVariante->pivot->stock : 0;
|
||||
} else {
|
||||
$bodegaProducto = $producto->bodegas()->where('bodega_id', $bodega->id)->first();
|
||||
$stockDisponible = $bodegaProducto ? $bodegaProducto->pivot->stock : 0;
|
||||
}
|
||||
|
||||
if ($stockDisponible > 0) {
|
||||
$cantidadADescontar = min($cantidadPendiente, $stockDisponible);
|
||||
|
||||
// Registrar detalle para esta bodega
|
||||
$venta->detalles()->create([
|
||||
'producto_id' => $item['id'],
|
||||
'variante_id' => $item['variante_id'] ?? null,
|
||||
'bodega_id' => $bodega->id,
|
||||
'cantidad' => $cantidadADescontar,
|
||||
'precio_unitario' => $precioModificado,
|
||||
'precio_original' => $precioOriginal,
|
||||
'descuento_aplicado' => $descuentoAplicado,
|
||||
'usuario_modifico_precio_id' => ($descuentoAplicado != 0) ? auth()->id() : null,
|
||||
'subtotal' => $cantidadADescontar * $precioModificado,
|
||||
]);
|
||||
|
||||
// Actualizar stock
|
||||
if ($variante) {
|
||||
$variante->bodegas()->updateExistingPivot($bodega->id, [
|
||||
'stock' => $stockDisponible - $cantidadADescontar
|
||||
]);
|
||||
} else {
|
||||
$producto->bodegas()->updateExistingPivot($bodega->id, [
|
||||
'stock' => $stockDisponible - $cantidadADescontar
|
||||
]);
|
||||
}
|
||||
|
||||
$cantidadPendiente -= $cantidadADescontar;
|
||||
}
|
||||
}
|
||||
|
||||
// Si aún queda cantidad pendiente (no había stock suficiente en ninguna bodega)
|
||||
// Lo asignamos a la Bodega Principal (o NULL si no hay bodegas) y dejamos que el stock se vaya a negativo o se descuente del directo
|
||||
if ($cantidadPendiente > 0) {
|
||||
$bodegaFallback = $bodegas->first(); // Principal por el ordenamiento
|
||||
$bodegaIdFallback = $bodegaFallback ? $bodegaFallback->id : null;
|
||||
|
||||
$venta->detalles()->create([
|
||||
'producto_id' => $item['id'],
|
||||
'variante_id' => $item['variante_id'] ?? null,
|
||||
'bodega_id' => $bodegaIdFallback,
|
||||
'cantidad' => $cantidadPendiente,
|
||||
'precio_unitario' => $precioModificado,
|
||||
'precio_original' => $precioOriginal,
|
||||
'descuento_aplicado' => $descuentoAplicado,
|
||||
'usuario_modifico_precio_id' => ($descuentoAplicado != 0) ? auth()->id() : null,
|
||||
'subtotal' => $cantidadPendiente * $precioModificado,
|
||||
]);
|
||||
|
||||
// Intentar descontar del fallback
|
||||
if ($bodegaFallback) {
|
||||
if ($variante) {
|
||||
$bodegaVariante = $variante->bodegas()->where('bodega_id', $bodegaFallback->id)->first();
|
||||
if ($bodegaVariante) {
|
||||
$variante->bodegas()->updateExistingPivot($bodegaFallback->id, [
|
||||
'stock' => $bodegaVariante->pivot->stock - $cantidadPendiente
|
||||
]);
|
||||
} else {
|
||||
// Si no existe la relación, la creamos con stock negativo
|
||||
$variante->bodegas()->attach($bodegaFallback->id, ['stock' => -$cantidadPendiente]);
|
||||
}
|
||||
} else {
|
||||
$bodegaProducto = $producto->bodegas()->where('bodega_id', $bodegaFallback->id)->first();
|
||||
if ($bodegaProducto) {
|
||||
$producto->bodegas()->updateExistingPivot($bodegaFallback->id, [
|
||||
'stock' => $bodegaProducto->pivot->stock - $cantidadPendiente
|
||||
]);
|
||||
} else {
|
||||
$producto->bodegas()->attach($bodegaFallback->id, ['stock' => -$cantidadPendiente]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Si no hay bodegas en absoluto, descontar del stock directo
|
||||
if ($variante) {
|
||||
$variante->decrement('stock', $cantidadPendiente);
|
||||
} else {
|
||||
$producto->decrement('stock', $cantidadPendiente);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cajaAbierta = Caja::where('estado', 'Abierta')
|
||||
->orderBy('fecha_apertura', 'desc')
|
||||
->first();
|
||||
|
||||
if ($cajaAbierta) {
|
||||
MovimientoCaja::create([
|
||||
'caja_id' => $cajaAbierta->id,
|
||||
'tipo' => 'Ingreso',
|
||||
'monto' => $this->total,
|
||||
'descripcion' => 'Venta realizada. Venta ID: ' . $venta->id,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
$this->resetVentaState();
|
||||
|
||||
session()->flash('message', 'Venta realizada con éxito.');
|
||||
$this->js("window.dispatchEvent(new CustomEvent('imprimir-recibo', { detail: { venta_id: {$venta->id} } }));");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
$this->resetVentaState();
|
||||
session()->flash('error', 'Error al procesar la venta. ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resetea el estado de la venta para evitar inconsistencias
|
||||
*/
|
||||
private function resetVentaState()
|
||||
{
|
||||
$this->carrito = [];
|
||||
$this->total = 0;
|
||||
$this->numeroDocumentoCliente = '';
|
||||
$this->datosCliente = [
|
||||
'nombre' => null,
|
||||
'correo' => null,
|
||||
'telefono' => null,
|
||||
];
|
||||
$this->cliente_id = null;
|
||||
$this->tipo_pago = 'Efectivo';
|
||||
|
||||
// Mantener la bodega seleccionada para la próxima venta
|
||||
// $this->bodega_id se mantiene
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function buscarProductoPorBarcode()
|
||||
{
|
||||
$barcode = trim($this->barcodeBusqueda);
|
||||
|
||||
$producto = Producto::where('codigo_barras', $barcode)->first();
|
||||
$tipo = 'p';
|
||||
|
||||
|
||||
if (!$producto) {
|
||||
$producto = ProductVariant::where('barcode', $barcode)->first();
|
||||
$tipo = 'v';
|
||||
}
|
||||
|
||||
if ($producto) {
|
||||
$this->agregarAlCarrito($producto->id, $tipo);
|
||||
$this->barcodeBusqueda = '';
|
||||
} else {
|
||||
session()->flash('error', 'Producto no encontrado.');
|
||||
}
|
||||
}
|
||||
|
||||
public function incrementarCantidad($key)
|
||||
{
|
||||
if (isset($this->carrito[$key])) {
|
||||
$this->carrito[$key]['cantidad']++;
|
||||
$this->calcularTotal();
|
||||
}
|
||||
}
|
||||
|
||||
public function decrementarCantidad($key)
|
||||
{
|
||||
if (isset($this->carrito[$key])) {
|
||||
$this->carrito[$key]['cantidad']--;
|
||||
|
||||
if ($this->carrito[$key]['cantidad'] <= 0) {
|
||||
unset($this->carrito[$key]);
|
||||
}
|
||||
|
||||
$this->calcularTotal();
|
||||
}
|
||||
}
|
||||
|
||||
public function eliminarDelCarrito($key)
|
||||
{
|
||||
if (isset($this->carrito[$key])) {
|
||||
unset($this->carrito[$key]);
|
||||
$this->calcularTotal();
|
||||
}
|
||||
}
|
||||
public function enviarCotizacion()
|
||||
{
|
||||
$correo = $this->correoCotizacion;
|
||||
|
||||
if (!filter_var($correo, FILTER_VALIDATE_EMAIL)) {
|
||||
session()->flash('error', 'Correo inválido.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$carrito = $this->carrito;
|
||||
$total = $this->total;
|
||||
|
||||
Mail::send('emails.cotizacion', compact('carrito', 'total'), function ($message) use ($correo) {
|
||||
$message->to($correo)
|
||||
->subject('Cotización de productos');
|
||||
});
|
||||
|
||||
session()->flash('message', 'Cotización enviada exitosamente.');
|
||||
$this->correoCotizacion = '';
|
||||
} catch (\Exception $e) {
|
||||
session()->flash('error', 'Error al enviar la cotización: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function buscarClientePorDocumento()
|
||||
{
|
||||
$documento = trim($this->numeroDocumentoCliente);
|
||||
|
||||
if (!$documento) {
|
||||
// No se requiere documento
|
||||
$this->cliente_id = null;
|
||||
$this->datosCliente = [
|
||||
'nombre' => null,
|
||||
'correo' => null,
|
||||
'telefono' => null,
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
$cliente = Cliente::where('numero_documento', $documento)->first();
|
||||
|
||||
if ($cliente) {
|
||||
// Verificar que el cliente encontrado existe realmente
|
||||
$clienteValidado = Cliente::find($cliente->id);
|
||||
if ($clienteValidado) {
|
||||
$this->cliente_id = $clienteValidado->id;
|
||||
$this->datosCliente = [
|
||||
'nombre' => $clienteValidado->nombre,
|
||||
'correo' => $clienteValidado->correo,
|
||||
'telefono' => $clienteValidado->telefono,
|
||||
];
|
||||
} else {
|
||||
// Cliente no válido, resetear
|
||||
$this->cliente_id = null;
|
||||
$this->datosCliente = [
|
||||
'nombre' => null,
|
||||
'correo' => null,
|
||||
'telefono' => null,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$this->cliente_id = null;
|
||||
$this->datosCliente = [
|
||||
'nombre' => null,
|
||||
'correo' => null,
|
||||
'telefono' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function cambiarBodega($bodegaId)
|
||||
{
|
||||
$this->bodega_id = $bodegaId;
|
||||
|
||||
// Opcional: mostrar notificación de cambio
|
||||
session()->flash('message', 'Bodega cambiada. Las ventas se realizarán desde la nueva bodega seleccionada.');
|
||||
}
|
||||
|
||||
public function getBodegasProperty()
|
||||
{
|
||||
return \App\Models\Bodega::all();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user