feat: agregar método para generar reporte de inventario en formato Excel
This commit is contained in:
parent
c68a16763c
commit
a0e8c70624
@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
|
use App\Models\Inventory;
|
||||||
use App\Models\Sale;
|
use App\Models\Sale;
|
||||||
|
use App\Models\SaleDetail;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
@ -453,6 +455,253 @@ public function salesReport(Request $request)
|
|||||||
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generar reporte Excel de inventario
|
||||||
|
*/
|
||||||
|
public function inventoryReport(Request $request)
|
||||||
|
{
|
||||||
|
// 1. VALIDACIÓN
|
||||||
|
$request->validate([
|
||||||
|
'category_id' => 'nullable|exists:categories,id',
|
||||||
|
'with_serials_only' => 'nullable|boolean',
|
||||||
|
'low_stock_threshold' => 'nullable|integer|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. CONSULTA DE INVENTARIO
|
||||||
|
$query = Inventory::with([
|
||||||
|
'category:id,name',
|
||||||
|
'price:inventory_id,retail_price',
|
||||||
|
'serials'
|
||||||
|
])
|
||||||
|
->when($request->category_id, function($q) use ($request) {
|
||||||
|
$q->where('category_id', $request->category_id);
|
||||||
|
})
|
||||||
|
->when($request->with_serials_only, function($q) {
|
||||||
|
$q->where('track_serials', true);
|
||||||
|
})
|
||||||
|
->when($request->low_stock_threshold, function($q) use ($request) {
|
||||||
|
$q->where('stock', '<=', $request->low_stock_threshold);
|
||||||
|
});
|
||||||
|
|
||||||
|
$inventories = $query->orderBy('name')->get();
|
||||||
|
|
||||||
|
if ($inventories->isEmpty()) {
|
||||||
|
return response()->json(['message' => 'No se encontraron productos'], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. MAPEO DE DATOS
|
||||||
|
$data = $inventories->map(function($inventory) {
|
||||||
|
// Cantidad vendida total
|
||||||
|
$quantitySold = SaleDetail::where('inventory_id', $inventory->id)
|
||||||
|
->whereHas('sale', function($q) {
|
||||||
|
$q->where('status', 'completed');
|
||||||
|
})
|
||||||
|
->sum('quantity');
|
||||||
|
|
||||||
|
// Conteo de seriales
|
||||||
|
$serialsTotal = $inventory->serials->count();
|
||||||
|
$serialsAvailable = $inventory->serials->where('status', 'disponible')->count();
|
||||||
|
$serialsSold = $inventory->serials->where('status', 'vendido')->count();
|
||||||
|
$serialsReturned = $inventory->serials->where('status', 'devuelto')->count();
|
||||||
|
|
||||||
|
$price = $inventory->price?->retail_price ?? 0;
|
||||||
|
$totalSold = $quantitySold * $price;
|
||||||
|
$inventoryValue = $inventory->stock * $price;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'sku' => $inventory->sku ?? 'N/A',
|
||||||
|
'name' => $inventory->name,
|
||||||
|
'category' => $inventory->category?->name ?? 'Sin categoría',
|
||||||
|
'stock' => $inventory->stock,
|
||||||
|
'quantity_sold' => $quantitySold,
|
||||||
|
'serials_total' => $serialsTotal,
|
||||||
|
'serials_available' => $serialsAvailable,
|
||||||
|
'serials_sold' => $serialsSold,
|
||||||
|
'serials_returned' => $serialsReturned,
|
||||||
|
'price' => (float) $price,
|
||||||
|
'total_sold' => $totalSold,
|
||||||
|
'inventory_value' => $inventoryValue,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. CONFIGURACIÓN EXCEL
|
||||||
|
$fileName = 'Reporte_Inventario_' . now()->format('Ymd_His') . '.xlsx';
|
||||||
|
$filePath = storage_path('app/temp/' . $fileName);
|
||||||
|
if (!file_exists(dirname($filePath))) mkdir(dirname($filePath), 0755, true);
|
||||||
|
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
|
||||||
|
// Fuente Global
|
||||||
|
$sheet->getParent()->getDefaultStyle()->getFont()->setName('Arial');
|
||||||
|
$sheet->getParent()->getDefaultStyle()->getFont()->setSize(10);
|
||||||
|
|
||||||
|
// Estilos Comunes
|
||||||
|
$styleBox = [
|
||||||
|
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => '000000']]],
|
||||||
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER]
|
||||||
|
];
|
||||||
|
$styleLabel = [
|
||||||
|
'font' => ['size' => 12, 'bold' => true],
|
||||||
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_RIGHT, 'vertical' => Alignment::VERTICAL_CENTER]
|
||||||
|
];
|
||||||
|
$styleTableHeader = [
|
||||||
|
'font' => ['bold' => true, 'size' => 10, 'color' => ['rgb' => 'FFFFFF']],
|
||||||
|
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '70AD47']],
|
||||||
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true],
|
||||||
|
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]]
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- ESTRUCTURA DEL DOCUMENTO ---
|
||||||
|
$lastCol = 'M';
|
||||||
|
$sheet->getRowDimension(2)->setRowHeight(10);
|
||||||
|
$sheet->getRowDimension(3)->setRowHeight(25);
|
||||||
|
$sheet->getRowDimension(5)->setRowHeight(30);
|
||||||
|
|
||||||
|
// --- TÍTULO PRINCIPAL ---
|
||||||
|
$sheet->mergeCells("A3:{$lastCol}3");
|
||||||
|
$sheet->setCellValue('A3', 'REPORTE DE INVENTARIO');
|
||||||
|
$sheet->getStyle('A3')->applyFromArray([
|
||||||
|
'font' => ['bold' => true, 'size' => 16, 'color' => ['rgb' => '000000']],
|
||||||
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// --- INFORMACIÓN DE FECHA ---
|
||||||
|
$sheet->mergeCells('A5:B5');
|
||||||
|
$sheet->setCellValue('A5', 'FECHA:');
|
||||||
|
$sheet->getStyle('A5')->applyFromArray($styleLabel);
|
||||||
|
|
||||||
|
$sheet->mergeCells("C5:{$lastCol}5");
|
||||||
|
Carbon::setLocale('es');
|
||||||
|
$sheet->setCellValue('C5', ucfirst(now()->translatedFormat('l j \d\e F Y')));
|
||||||
|
$sheet->getStyle("C5:{$lastCol}5")->applyFromArray($styleBox);
|
||||||
|
$sheet->getStyle('C5')->getFont()->setSize(12);
|
||||||
|
|
||||||
|
// --- RESUMEN DE TOTALES ---
|
||||||
|
$totalProducts = $data->count();
|
||||||
|
$totalStock = $data->sum('stock');
|
||||||
|
$totalQuantitySold = $data->sum('quantity_sold');
|
||||||
|
$totalSoldValue = $data->sum('total_sold');
|
||||||
|
$totalInventoryValue = $data->sum('inventory_value');
|
||||||
|
|
||||||
|
$row = 7;
|
||||||
|
$sheet->setCellValue('A' . $row, 'TOTAL PRODUCTOS:');
|
||||||
|
$sheet->setCellValue('C' . $row, $totalProducts);
|
||||||
|
$sheet->setCellValue('D' . $row, 'STOCK TOTAL:');
|
||||||
|
$sheet->setCellValue('F' . $row, $totalStock);
|
||||||
|
$sheet->setCellValue('G' . $row, 'VENDIDOS:');
|
||||||
|
$sheet->setCellValue('H' . $row, $totalQuantitySold);
|
||||||
|
$sheet->setCellValue('I' . $row, 'TOTAL VENDIDO:');
|
||||||
|
$sheet->setCellValue('K' . $row, '$' . number_format($totalSoldValue, 2));
|
||||||
|
$sheet->setCellValue('L' . $row, 'VALOR INVENTARIO:');
|
||||||
|
$sheet->setCellValue('M' . $row, '$' . number_format($totalInventoryValue, 2));
|
||||||
|
|
||||||
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->getFont()->setBold(true);
|
||||||
|
$sheet->getStyle('C' . $row)->getFont()->setSize(12)->getColor()->setRGB('70AD47');
|
||||||
|
$sheet->getStyle('F' . $row)->getFont()->setSize(12)->getColor()->setRGB('70AD47');
|
||||||
|
$sheet->getStyle('H' . $row)->getFont()->setSize(12)->getColor()->setRGB('2E75B6');
|
||||||
|
$sheet->getStyle('K' . $row)->getFont()->setSize(12)->getColor()->setRGB('FF6600');
|
||||||
|
$sheet->getStyle('M' . $row)->getFont()->setSize(12)->getColor()->setRGB('008000');
|
||||||
|
|
||||||
|
// --- ENCABEZADOS DE TABLA ---
|
||||||
|
$h = 9;
|
||||||
|
$headers = [
|
||||||
|
'A' => 'No.',
|
||||||
|
'B' => 'SKU',
|
||||||
|
'C' => 'NOMBRE',
|
||||||
|
'D' => 'CATEGORÍA',
|
||||||
|
'E' => "STOCK\nDISPONIBLE",
|
||||||
|
'F' => "CANTIDAD\nVENDIDA",
|
||||||
|
'G' => "SERIALES\nTOTALES",
|
||||||
|
'H' => "SERIALES\nDISPONIBLES",
|
||||||
|
'I' => "SERIALES\nVENDIDOS",
|
||||||
|
'J' => "SERIALES\nDEVUELTOS",
|
||||||
|
'K' => "PRECIO\nUNITARIO",
|
||||||
|
'L' => "TOTAL\nVENDIDO",
|
||||||
|
'M' => "VALOR\nINVENTARIO",
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($headers as $col => $text) {
|
||||||
|
$sheet->setCellValue("{$col}{$h}", $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->getStyle("A{$h}:{$lastCol}{$h}")->applyFromArray($styleTableHeader);
|
||||||
|
$sheet->getRowDimension($h)->setRowHeight(35);
|
||||||
|
|
||||||
|
// --- LLENADO DE DATOS ---
|
||||||
|
$row = 10;
|
||||||
|
$i = 1;
|
||||||
|
|
||||||
|
foreach ($data as $item) {
|
||||||
|
$sheet->setCellValue('A' . $row, $i);
|
||||||
|
$sheet->setCellValue('B' . $row, $item['sku']);
|
||||||
|
$sheet->setCellValue('C' . $row, $item['name']);
|
||||||
|
$sheet->setCellValue('D' . $row, $item['category']);
|
||||||
|
$sheet->setCellValue('E' . $row, $item['stock']);
|
||||||
|
$sheet->setCellValue('F' . $row, $item['quantity_sold']);
|
||||||
|
$sheet->setCellValue('G' . $row, $item['serials_total']);
|
||||||
|
$sheet->setCellValue('H' . $row, $item['serials_available']);
|
||||||
|
$sheet->setCellValue('I' . $row, $item['serials_sold']);
|
||||||
|
$sheet->setCellValue('J' . $row, $item['serials_returned']);
|
||||||
|
$sheet->setCellValue('K' . $row, '$' . number_format($item['price'], 2));
|
||||||
|
$sheet->setCellValue('L' . $row, '$' . number_format($item['total_sold'], 2));
|
||||||
|
$sheet->setCellValue('M' . $row, '$' . number_format($item['inventory_value'], 2));
|
||||||
|
|
||||||
|
// Estilos de fila
|
||||||
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([
|
||||||
|
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||||
|
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||||
|
'font' => ['size' => 10]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Centrados
|
||||||
|
$sheet->getStyle("A{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("B{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("E{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("F{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("G{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("H{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("I{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
$sheet->getStyle("J{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||||
|
|
||||||
|
// Color de stock bajo (si está configurado el threshold)
|
||||||
|
if ($request->low_stock_threshold && $item['stock'] <= $request->low_stock_threshold) {
|
||||||
|
$sheet->getStyle("E{$row}")->getFont()->getColor()->setRGB('FF0000');
|
||||||
|
$sheet->getStyle("E{$row}")->getFont()->setBold(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color alterno de filas
|
||||||
|
if ($i % 2 == 0) {
|
||||||
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->getFill()
|
||||||
|
->setFillType(Fill::FILL_SOLID)
|
||||||
|
->getStartColor()->setRGB('F2F2F2');
|
||||||
|
}
|
||||||
|
|
||||||
|
$row++;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- ANCHOS DE COLUMNA ---
|
||||||
|
$sheet->getColumnDimension('A')->setWidth(5);
|
||||||
|
$sheet->getColumnDimension('B')->setWidth(15);
|
||||||
|
$sheet->getColumnDimension('C')->setWidth(35);
|
||||||
|
$sheet->getColumnDimension('D')->setWidth(18);
|
||||||
|
$sheet->getColumnDimension('E')->setWidth(12);
|
||||||
|
$sheet->getColumnDimension('F')->setWidth(12);
|
||||||
|
$sheet->getColumnDimension('G')->setWidth(12);
|
||||||
|
$sheet->getColumnDimension('H')->setWidth(14);
|
||||||
|
$sheet->getColumnDimension('I')->setWidth(12);
|
||||||
|
$sheet->getColumnDimension('J')->setWidth(12);
|
||||||
|
$sheet->getColumnDimension('K')->setWidth(18);
|
||||||
|
$sheet->getColumnDimension('L')->setWidth(14);
|
||||||
|
$sheet->getColumnDimension('M')->setWidth(16);
|
||||||
|
|
||||||
|
$writer = new Xlsx($spreadsheet);
|
||||||
|
$writer->save($filePath);
|
||||||
|
|
||||||
|
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traducir método de pago
|
* Traducir método de pago
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -129,6 +129,18 @@ public function run(): void
|
|||||||
$clientTierDestroy
|
$clientTierDestroy
|
||||||
] = $this->onCRUD('client-tiers', $clientTiersType, 'api');
|
] = $this->onCRUD('client-tiers', $clientTiersType, 'api');
|
||||||
|
|
||||||
|
// Permisos de Solicitudes de Factura
|
||||||
|
$invoiceRequestsType = PermissionType::firstOrCreate([
|
||||||
|
'name' => 'Solicitudes de factura'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$invoiceRequestIndex = $this->onIndex('invoice-requests', 'Mostrar solicitudes', $invoiceRequestsType, 'api');
|
||||||
|
$invoiceRequestShow = $this->onPermission('invoice-requests.show', 'Ver detalles', $invoiceRequestsType, 'api');
|
||||||
|
$invoiceRequestProcess = $this->onPermission('invoice-requests.process', 'Procesar solicitud', $invoiceRequestsType, 'api');
|
||||||
|
$invoiceRequestReject = $this->onPermission('invoice-requests.reject', 'Rechazar solicitud', $invoiceRequestsType, 'api');
|
||||||
|
$invoiceRequestUpload = $this->onPermission('invoice-requests.upload', 'Subir archivos de factura', $invoiceRequestsType, 'api');
|
||||||
|
$invoiceRequestStats = $this->onPermission('invoice-requests.stats', 'Ver estadísticas', $invoiceRequestsType, 'api');
|
||||||
|
|
||||||
|
|
||||||
// ==================== ROLES ====================
|
// ==================== ROLES ====================
|
||||||
|
|
||||||
@ -177,7 +189,13 @@ public function run(): void
|
|||||||
$clientTierIndex,
|
$clientTierIndex,
|
||||||
$clientTierCreate,
|
$clientTierCreate,
|
||||||
$clientTierEdit,
|
$clientTierEdit,
|
||||||
$clientTierDestroy
|
$clientTierDestroy,
|
||||||
|
$invoiceRequestIndex,
|
||||||
|
$invoiceRequestShow,
|
||||||
|
$invoiceRequestProcess,
|
||||||
|
$invoiceRequestReject,
|
||||||
|
$invoiceRequestUpload,
|
||||||
|
$invoiceRequestStats
|
||||||
);
|
);
|
||||||
|
|
||||||
//Operador PDV (solo permisos de operación de caja y ventas)
|
//Operador PDV (solo permisos de operación de caja y ventas)
|
||||||
@ -205,7 +223,9 @@ public function run(): void
|
|||||||
$clientTierIndex,
|
$clientTierIndex,
|
||||||
$clientTierCreate,
|
$clientTierCreate,
|
||||||
$clientTierEdit,
|
$clientTierEdit,
|
||||||
$clientTierDestroy
|
$clientTierDestroy,
|
||||||
|
// Solicitudes de factura (solo lectura)
|
||||||
|
$invoiceRequestIndex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,6 +78,7 @@
|
|||||||
Route::get('products-without-movement', [ReportController::class, 'productsWithoutMovement']);
|
Route::get('products-without-movement', [ReportController::class, 'productsWithoutMovement']);
|
||||||
Route::get('client-discounts/excel', [ExcelController::class, 'clientDiscountsReport']);
|
Route::get('client-discounts/excel', [ExcelController::class, 'clientDiscountsReport']);
|
||||||
Route::get('sales/excel', [ExcelController::class, 'salesReport']);
|
Route::get('sales/excel', [ExcelController::class, 'salesReport']);
|
||||||
|
Route::get('inventory/excel', [ExcelController::class, 'inventoryReport']);
|
||||||
});
|
});
|
||||||
|
|
||||||
//CLIENTES
|
//CLIENTES
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user