This commit is contained in:
Lizandro Guarnizo
2026-01-06 15:35:59 -05:00
commit a768146f65
602 changed files with 42505 additions and 0 deletions
@@ -0,0 +1,43 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use App\Mail\ReporteVentasMail;
use App\Models\User;
use App\Models\Venta;
use Carbon\Carbon;
class EnviarReporteVentas extends Command
{
protected $signature = 'reporte:ventas';
protected $description = 'Enviar reporte de ventas semanal por correo';
public function handle()
{
$fromDate = Carbon::now()->subWeek()->startOfWeek(); // Lunes anterior
$toDate = Carbon::now()->subWeek()->endOfWeek(); // Domingo anterior
$ventas = Venta::with(['cliente', 'detalles.producto', 'detalles.variante'])
->whereBetween('created_at', [$fromDate, $toDate])
->get();
// Obtener todos los usuarios con el rol 'Administrador'
$administradores = User::role('Administrador')->get();
if ($administradores->isEmpty()) {
$this->warn('No se encontraron usuarios con el rol Administrador.');
return;
}
foreach ($administradores as $admin) {
Mail::to($admin->email)->send(new ReporteVentasMail($ventas, $fromDate, $toDate, $admin->email));
$this->info("Reporte enviado a: {$admin->email}");
}
$this->info('Todos los reportes han sido enviados a los administradores.');
}
}
@@ -0,0 +1,115 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Bodega;
use App\Models\Producto;
use Illuminate\Support\Facades\DB;
class MigrarStockBodegaPrincipal extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'stock:migrar-principal';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migra todo el stock actual de productos a la bodega principal';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Iniciando migración de stock a bodega principal...');
DB::beginTransaction();
try {
// 1. Crear o encontrar la bodega principal
$bodegaPrincipal = Bodega::firstOrCreate([
'nombre' => 'Principal'
]);
$this->info("Bodega principal creada/encontrada: {$bodegaPrincipal->nombre}");
// 2. Obtener todos los productos que necesitan migración
$productos = Producto::with('variants')->get()->filter(function ($producto) {
// Productos con stock directo O con variantes que tienen stock
return $producto->stock > 0 ||
($producto->variants()->exists() && $producto->variants()->sum('stock') > 0);
});
$this->info("Encontrados {$productos->count()} productos con stock para migrar");
$productosActualizados = 0;
foreach ($productos as $producto) {
// Calcular stock total a migrar
$stockAMigrar = 0;
if ($producto->variants()->exists()) {
// Para productos con variantes, usar la suma de variantes
$stockAMigrar = $producto->variants()->sum('stock');
$this->line(" → Producto con variantes: {$producto->nombre}");
$this->line(" Stock total de variantes: {$stockAMigrar}");
} else {
// Para productos sin variantes, usar stock directo
$stockAMigrar = $producto->stock;
}
if ($stockAMigrar <= 0) {
continue; // Saltar productos sin stock
}
// Verificar si ya existe una relación con esta bodega
$existeRelacion = $producto->bodegas()
->where('bodega_id', $bodegaPrincipal->id)
->exists();
if (!$existeRelacion) {
// Crear la relación con el stock calculado
$producto->bodegas()->attach($bodegaPrincipal->id, [
'stock' => $stockAMigrar
]);
$this->line("{$producto->nombre}: {$stockAMigrar} unidades → Bodega Principal");
$productosActualizados++;
} else {
// Actualizar el stock existente sumando el stock calculado
$stockActual = $producto->bodegas()
->where('bodega_id', $bodegaPrincipal->id)
->first()->pivot->stock;
$nuevoStock = $stockActual + $stockAMigrar;
$producto->bodegas()->updateExistingPivot($bodegaPrincipal->id, [
'stock' => $nuevoStock
]);
$this->line("{$producto->nombre}: {$stockActual} + {$stockAMigrar} = {$nuevoStock} unidades → Bodega Principal");
$productosActualizados++;
}
}
DB::commit();
$this->info("\n🎉 Migración completada exitosamente!");
$this->info("📦 Productos procesados: {$productosActualizados}");
$this->info("🏢 Bodega principal: {$bodegaPrincipal->nombre}");
} catch (\Exception $e) {
DB::rollBack();
$this->error("❌ Error durante la migración: " . $e->getMessage());
return Command::FAILURE;
}
return Command::SUCCESS;
}
}
@@ -0,0 +1,144 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\Bodega;
use App\Models\ProductVariant;
use App\Models\Producto;
use Illuminate\Support\Facades\DB;
class MigrarVariantesBodegaPrincipal extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'stock:migrar-todo-principal';
/**
* The console command description.
*/
protected $description = 'Migra el stock de productos y variantes a la bodega principal';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('🚀 Iniciando migración completa de stock a bodega principal...');
DB::beginTransaction();
try {
// 1. Crear o encontrar la bodega principal
$bodegaPrincipal = Bodega::firstOrCreate([
'nombre' => 'Principal'
], [
'descripcion' => 'Bodega principal del sistema',
'ubicacion' => 'Sede principal',
'estado' => true
]);
$this->info("✅ Bodega principal: {$bodegaPrincipal->nombre}");
// 2. MIGRAR PRODUCTOS DIRECTOS (sin variantes)
$this->info("\n📦 Migrando productos directos...");
$productosDirectos = Producto::where('estado', true)
->where('stock', '>', 0)
->whereDoesntHave('variants') // Solo productos sin variantes
->get();
$this->info("Encontrados {$productosDirectos->count()} productos directos con stock");
$productosActualizados = 0;
foreach ($productosDirectos as $producto) {
// Verificar si ya existe una relación con esta bodega
$existeRelacion = $producto->bodegas()
->where('bodega_id', $bodegaPrincipal->id)
->exists();
if (!$existeRelacion) {
// Crear la relación con el stock actual del producto
$producto->bodegas()->attach($bodegaPrincipal->id, [
'stock' => $producto->stock
]);
$this->line("{$producto->nombre}: {$producto->stock} unidades → Bodega Principal");
$productosActualizados++;
} else {
// Actualizar el stock existente
$stockActual = $producto->bodegas()
->where('bodega_id', $bodegaPrincipal->id)
->first()->pivot->stock;
$nuevoStock = $stockActual + $producto->stock;
$producto->bodegas()->updateExistingPivot($bodegaPrincipal->id, [
'stock' => $nuevoStock
]);
$this->line("{$producto->nombre}: {$stockActual} + {$producto->stock} = {$nuevoStock} unidades → Bodega Principal");
$productosActualizados++;
}
}
// 3. MIGRAR VARIANTES DE PRODUCTOS
$this->info("\n🔄 Migrando variantes de productos...");
$variantes = ProductVariant::with('producto')
->where('stock', '>', 0)
->get();
$this->info("Encontradas {$variantes->count()} variantes con stock");
$variantesActualizadas = 0;
foreach ($variantes as $variante) {
// Verificar si ya existe una relación con esta bodega
$existeRelacion = $variante->bodegas()
->where('bodega_id', $bodegaPrincipal->id)
->exists();
if (!$existeRelacion) {
// Crear la relación con el stock actual de la variante
$variante->bodegas()->attach($bodegaPrincipal->id, [
'stock' => $variante->stock
]);
$this->line("{$variante->producto->nombre} [{$variante->sku}]: {$variante->stock} unidades → Bodega Principal");
$variantesActualizadas++;
} else {
// Actualizar el stock existente sumando el stock de la variante
$stockActual = $variante->bodegas()
->where('bodega_id', $bodegaPrincipal->id)
->first()->pivot->stock;
$nuevoStock = $stockActual + $variante->stock;
$variante->bodegas()->updateExistingPivot($bodegaPrincipal->id, [
'stock' => $nuevoStock
]);
$this->line("{$variante->producto->nombre} [{$variante->sku}]: {$stockActual} + {$variante->stock} = {$nuevoStock} unidades → Bodega Principal");
$variantesActualizadas++;
}
}
DB::commit();
$this->info("\n🎉 Migración completa de stock exitosa!");
$this->info("📦 Productos directos procesados: {$productosActualizados}");
$this->info(" Variantes procesadas: {$variantesActualizadas}");
$this->info("🏢 Bodega principal: {$bodegaPrincipal->nombre}");
$this->info("📊 Total de elementos migrados: " . ($productosActualizados + $variantesActualizadas));
} catch (\Exception $e) {
DB::rollBack();
$this->error("❌ Error durante la migración: " . $e->getMessage());
$this->error("🔍 Línea: " . $e->getLine());
return Command::FAILURE;
}
return Command::SUCCESS;
}
}
@@ -0,0 +1,153 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
use App\Mail\AlertaStockMail;
use App\Models\User;
use App\Models\Producto;
class VerificarAlertasStock extends Command
{
protected $signature = 'stock:verificar-alertas {--email= : Email específico para enviar la alerta}';
protected $description = 'Verificar niveles de stock y enviar alertas por correo cuando se alcance el mínimo o máximo';
public function handle()
{
$this->info('🔍 Iniciando verificación de niveles de stock...');
// Obtener productos con stock mínimo o inferior
$productosStockMinimo = $this->obtenerProductosStockMinimo();
// Obtener productos que exceden el stock máximo
$productosStockMaximo = $this->obtenerProductosStockMaximo();
if ($productosStockMinimo->isEmpty() && $productosStockMaximo->isEmpty()) {
$this->info('✅ Todos los productos están en niveles normales de stock.');
return Command::SUCCESS;
}
// Mostrar resumen en consola
$this->mostrarResumen($productosStockMinimo, $productosStockMaximo);
// Enviar alertas por correo
$this->enviarAlertas($productosStockMinimo, $productosStockMaximo);
return Command::SUCCESS;
}
private function obtenerProductosStockMinimo()
{
return Producto::with(['categoria', 'variants', 'bodegas'])
->where('estado', true)
->get()
->filter(function ($producto) {
// Usar el método getStockEfectivo que considera bodegas
$stockActual = $producto->getStockEfectivo();
return $stockActual <= $producto->stock_minimo;
});
}
private function obtenerProductosStockMaximo()
{
return Producto::with(['categoria', 'variants', 'bodegas'])
->where('estado', true)
->whereNotNull('stock_maximo')
->get()
->filter(function ($producto) {
// Usar el método getStockEfectivo que considera bodegas
$stockActual = $producto->getStockEfectivo();
return $stockActual > $producto->stock_maximo;
});
}
private function mostrarResumen($productosStockMinimo, $productosStockMaximo)
{
if ($productosStockMinimo->isNotEmpty()) {
$this->warn("⚠️ {$productosStockMinimo->count()} productos con stock mínimo o inferior:");
$headers = ['Producto', 'Stock Actual', 'Stock Mínimo', 'Estado'];
$rows = [];
foreach ($productosStockMinimo as $producto) {
$stockActual = $producto->getStockEfectivo();
$estado = $stockActual == 0 ? 'Sin Stock' : ($stockActual < $producto->stock_minimo ? 'Por debajo' : 'En mínimo');
$rows[] = [
$producto->nombre,
$stockActual,
$producto->stock_minimo,
$estado
];
}
$this->table($headers, $rows);
}
if ($productosStockMaximo->isNotEmpty()) {
$this->info("📈 {$productosStockMaximo->count()} productos que exceden el stock máximo:");
$headers = ['Producto', 'Stock Actual', 'Stock Máximo', 'Exceso'];
$rows = [];
foreach ($productosStockMaximo as $producto) {
$stockActual = $producto->getStockEfectivo();
$exceso = $stockActual - $producto->stock_maximo;
$rows[] = [
$producto->nombre,
$stockActual,
$producto->stock_maximo,
"+{$exceso}"
];
}
$this->table($headers, $rows);
}
}
private function enviarAlertas($productosStockMinimo, $productosStockMaximo)
{
// Determinar destinatarios
$email = $this->option('email');
if ($email) {
$destinatarios = collect([$email]);
$this->info("📧 Enviando alerta a email específico: {$email}");
} else {
// Obtener administradores
$administradores = User::role('Administrador')->get();
if ($administradores->isEmpty()) {
$this->warn('⚠️ No se encontraron usuarios con el rol Administrador.');
return;
}
$destinatarios = $administradores->pluck('email');
$this->info("📧 Enviando alertas a {$administradores->count()} administradores...");
}
// Enviar correos
foreach ($destinatarios as $emailDestino) {
try {
Mail::to($emailDestino)->send(new AlertaStockMail(
$productosStockMinimo,
$productosStockMaximo,
$emailDestino
));
$this->info("✅ Alerta enviada a: {$emailDestino}");
} catch (\Exception $e) {
$this->error("❌ Error al enviar a {$emailDestino}: " . $e->getMessage());
}
}
$this->info('🎉 Proceso de envío de alertas completado.');
}
}