validate([ 'inventory_id' => 'nullable|exists:inventories,id', 'warehouse_id' => 'nullable|exists:warehouses,id', 'fecha_inicio' => 'required|date', 'fecha_fin' => 'required|date|after_or_equal:fecha_inicio', 'movement_type' => 'nullable|in:entry,exit,transfer,sale,return', ]); $fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay(); $fechaFin = Carbon::parse($request->fecha_fin)->endOfDay(); // Si no hay inventory_id, mostrar todos los movimientos if (!$request->inventory_id) { return $this->exportAllMovements($request, $fechaInicio, $fechaFin); } $inventory = Inventory::with(['category', 'price'])->findOrFail($request->inventory_id); // 2. CONSULTA DE MOVIMIENTOS $query = InventoryMovement::where('inventory_id', $inventory->id) ->with(['warehouseFrom', 'warehouseTo', 'user']) ->whereBetween('created_at', [$fechaInicio, $fechaFin]) ->orderBy('created_at', 'asc') ->orderBy('id', 'asc'); $warehouseId = $request->warehouse_id; if ($warehouseId) { $query->where(function ($q) use ($warehouseId) { $q->where('warehouse_from_id', $warehouseId) ->orWhere('warehouse_to_id', $warehouseId); }); } if ($request->movement_type) { $query->where('movement_type', $request->movement_type); } $movements = $query->get(); // 3. CONSTRUIR LÍNEAS DEL KARDEX $kardexLines = []; foreach ($movements as $movement) { $kardexLines[] = [ 'date' => $movement->created_at->format('d/m/Y H:i'), 'type' => $this->translateMovementType($movement->movement_type), 'warehouse_from' => $movement->warehouseFrom?->name ?? '-', 'warehouse_to' => $movement->warehouseTo?->name ?? '-', 'quantity' => $movement->quantity, 'unit_cost' => $movement->unit_cost ?? 0, 'notes' => $movement->notes ?? '', 'invoice_reference' => $movement->invoice_reference ?? '', 'user' => $movement->user?->name ?? '', ]; } if (empty($kardexLines)) { return response()->json(['message' => 'No se encontraron movimientos en el periodo especificado'], 404); } // 5. GENERAR EXCEL $fileName = 'Kardex_' . ($inventory->sku ?? $inventory->id) . '_' . $fechaInicio->format('Ymd') . '_' . $fechaFin->format('Ymd') . '.xlsx'; $filePath = storage_path('app/temp/' . $fileName); if (!file_exists(dirname($filePath))) mkdir(dirname($filePath), 0755, true); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Kardex'); // Fuente global $sheet->getParent()->getDefaultStyle()->getFont()->setName('Arial'); // Estilos $styleLabel = [ 'font' => ['bold' => true, 'size' => 10], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]; $styleBox = [ 'borders' => ['outline' => ['borderStyle' => Border::BORDER_THIN]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]; $styleTableHeader = [ 'font' => ['bold' => true, 'size' => 10, 'color' => ['rgb' => 'FFFFFF']], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '2E75B6']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]; $lastCol = 'J'; // --- TÍTULO --- $sheet->mergeCells("A2:{$lastCol}2"); $sheet->setCellValue('A2', 'KARDEX DE PRODUCTO'); $sheet->getStyle('A2')->applyFromArray([ 'font' => ['bold' => true, 'size' => 16, 'color' => ['rgb' => '2E75B6']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getRowDimension(2)->setRowHeight(30); // --- INFORMACIÓN DEL PRODUCTO --- $row = 4; $sheet->setCellValue("A{$row}", 'PRODUCTO:'); $sheet->getStyle("A{$row}")->applyFromArray($styleLabel); $sheet->mergeCells("B{$row}:E{$row}"); $sheet->setCellValue("B{$row}", $inventory->name); $sheet->getStyle("B{$row}:E{$row}")->applyFromArray($styleBox); $sheet->setCellValue("G{$row}", 'SKU:'); $sheet->getStyle("G{$row}")->applyFromArray($styleLabel); $sheet->mergeCells("H{$row}:{$lastCol}{$row}"); $sheet->setCellValue("H{$row}", $inventory->sku ?? 'N/A'); $sheet->getStyle("H{$row}:{$lastCol}{$row}")->applyFromArray($styleBox); $row++; $sheet->setCellValue("A{$row}", 'CATEGORÍA:'); $sheet->getStyle("A{$row}")->applyFromArray($styleLabel); $sheet->mergeCells("B{$row}:E{$row}"); $sheet->setCellValue("B{$row}", $inventory->category?->name ?? 'Sin categoría'); $sheet->getStyle("B{$row}:E{$row}")->applyFromArray($styleBox); $sheet->setCellValue("G{$row}", 'COSTO ACTUAL:'); $sheet->getStyle("G{$row}")->applyFromArray($styleLabel); $sheet->mergeCells("H{$row}:{$lastCol}{$row}"); $sheet->setCellValue("H{$row}", '$' . number_format($inventory->price?->cost ?? 0, 2)); $sheet->getStyle("H{$row}:{$lastCol}{$row}")->applyFromArray($styleBox); // --- PERÍODO --- $row++; Carbon::setLocale('es'); if ($fechaInicio->format('m/Y') === $fechaFin->format('m/Y')) { $periodoTexto = 'del ' . $fechaInicio->format('d') . ' al ' . $fechaFin->format('d') . ' de ' . $fechaFin->translatedFormat('F \d\e Y'); } else { $periodoTexto = 'del ' . $fechaInicio->format('d/m/Y') . ' al ' . $fechaFin->format('d/m/Y'); } $sheet->setCellValue("A{$row}", 'PERÍODO:'); $sheet->getStyle("A{$row}")->applyFromArray($styleLabel); $sheet->mergeCells("B{$row}:{$lastCol}{$row}"); $sheet->setCellValue("B{$row}", $periodoTexto); $sheet->getStyle("B{$row}:{$lastCol}{$row}")->applyFromArray($styleBox); $sheet->getStyle("B{$row}")->getFont()->setSize(11); // --- ENCABEZADOS DE TABLA --- $row += 2; $headerRow = $row; $headers = [ 'A' => 'FECHA', 'B' => 'TIPO', 'C' => "ALMACÉN\nORIGEN", 'D' => "ALMACÉN\nDESTINO", 'E' => 'CANTIDAD', 'F' => "COSTO\nUNITARIO", 'G' => 'REFERENCIA', 'H' => 'NOTAS', 'I' => 'USUARIO', ]; foreach ($headers as $col => $text) { $sheet->setCellValue("{$col}{$row}", $text); } $sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray($styleTableHeader); $sheet->getRowDimension($row)->setRowHeight(30); // --- DATOS --- $row++; $totalQuantity = 0; foreach ($kardexLines as $line) { $sheet->setCellValue("A{$row}", $line['date']); $sheet->setCellValue("B{$row}", $line['type']); $sheet->setCellValue("C{$row}", $line['warehouse_from']); $sheet->setCellValue("D{$row}", $line['warehouse_to']); $sheet->setCellValue("E{$row}", $line['quantity']); $sheet->setCellValue("F{$row}", $line['unit_cost'] ? '$' . number_format($line['unit_cost'], 2) : ''); $sheet->setCellValue("G{$row}", $line['invoice_reference']); $sheet->setCellValue("H{$row}", $line['notes']); $sheet->setCellValue("I{$row}", $line['user']); $totalQuantity += $line['quantity']; // Estilo de fila $sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], 'font' => ['size' => 10], ]); // Color alterno if (($row - $headerRow) % 2 === 0) { $sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([ 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'D9E2F3']], ]); } $row++; } // --- FILA DE TOTALES --- $sheet->mergeCells("A{$row}:D{$row}"); $sheet->setCellValue("A{$row}", 'TOTALES'); $sheet->setCellValue("E{$row}", $totalQuantity); $sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([ 'font' => ['bold' => true, 'size' => 10], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '2E75B6']], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getStyle("A{$row}:{$lastCol}{$row}")->getFont()->setColor(new Color('FFFFFF')); // --- ANCHOS DE COLUMNA --- $sheet->getColumnDimension('A')->setWidth(18); $sheet->getColumnDimension('B')->setWidth(14); $sheet->getColumnDimension('C')->setWidth(18); $sheet->getColumnDimension('D')->setWidth(18); $sheet->getColumnDimension('E')->setWidth(12); $sheet->getColumnDimension('F')->setWidth(14); $sheet->getColumnDimension('G')->setWidth(18); $sheet->getColumnDimension('H')->setWidth(25); $sheet->getColumnDimension('I')->setWidth(16); $sheet->getColumnDimension('J')->setWidth(16); // 6. GUARDAR Y DESCARGAR $writer = new Xlsx($spreadsheet); $writer->save($filePath); return response()->download($filePath, $fileName)->deleteFileAfterSend(true); } /** * Traducir tipo de movimiento */ private function translateMovementType(string $type): string { return match ($type) { 'entry' => 'Entrada', 'exit' => 'Salida', 'transfer' => 'Traspaso', /* 'sale' => 'Venta', 'return' => 'Devolución', */ default => $type, }; } /** * Exportar todos los movimientos (sin filtro de producto) */ private function exportAllMovements(Request $request, Carbon $fechaInicio, Carbon $fechaFin) { // CONSULTA DE MOVIMIENTOS $query = InventoryMovement::with(['inventory', 'warehouseFrom', 'warehouseTo', 'user']) ->whereBetween('created_at', [$fechaInicio, $fechaFin]) ->orderBy('created_at', 'asc') ->orderBy('id', 'asc'); if ($request->warehouse_id) { $query->where(function ($q) use ($request) { $q->where('warehouse_from_id', $request->warehouse_id) ->orWhere('warehouse_to_id', $request->warehouse_id); }); } if ($request->movement_type) { $query->where('movement_type', $request->movement_type); } $movements = $query->get(); if ($movements->isEmpty()) { return response()->json(['message' => 'No se encontraron movimientos en el periodo especificado'], 404); } // GENERAR EXCEL $fileName = 'Movimientos_' . $fechaInicio->format('Ymd') . '_' . $fechaFin->format('Ymd') . '.xlsx'; $filePath = storage_path('app/temp/' . $fileName); if (!file_exists(dirname($filePath))) mkdir(dirname($filePath), 0755, true); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Movimientos'); $sheet->getParent()->getDefaultStyle()->getFont()->setName('Arial'); $styleTableHeader = [ 'font' => ['bold' => true, 'size' => 10, 'color' => ['rgb' => 'FFFFFF']], 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '2E75B6']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]; $lastCol = 'J'; // TÍTULO $sheet->mergeCells("A2:{$lastCol}2"); $sheet->setCellValue('A2', 'REPORTE DE MOVIMIENTOS DE INVENTARIO'); $sheet->getStyle('A2')->applyFromArray([ 'font' => ['bold' => true, 'size' => 16, 'color' => ['rgb' => '2E75B6']], 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getRowDimension(2)->setRowHeight(30); // PERÍODO $row = 4; Carbon::setLocale('es'); $periodoTexto = 'del ' . $fechaInicio->format('d/m/Y') . ' al ' . $fechaFin->format('d/m/Y'); $sheet->setCellValue("A{$row}", 'PERÍODO:'); $sheet->getStyle("A{$row}")->getFont()->setBold(true); $sheet->mergeCells("B{$row}:{$lastCol}{$row}"); $sheet->setCellValue("B{$row}", $periodoTexto); $sheet->getStyle("B{$row}:{$lastCol}{$row}")->applyFromArray([ 'borders' => ['outline' => ['borderStyle' => Border::BORDER_THIN]], ]); // ENCABEZADOS $row += 2; $headerRow = $row; $headers = [ 'A' => 'FECHA', 'B' => 'PRODUCTO', 'C' => 'TIPO', 'D' => "ALMACÉN\nORIGEN", 'E' => "ALMACÉN\nDESTINO", 'F' => 'CANTIDAD', 'G' => "COSTO\nUNITARIO", 'H' => 'REFERENCIA', 'I' => 'NOTAS', 'J' => 'USUARIO', ]; foreach ($headers as $col => $text) { $sheet->setCellValue("{$col}{$row}", $text); } $sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray($styleTableHeader); $sheet->getRowDimension($row)->setRowHeight(30); // DATOS $row++; foreach ($movements as $movement) { $sheet->setCellValue("A{$row}", $movement->created_at->format('d/m/Y H:i')); $sheet->setCellValue("B{$row}", $movement->inventory?->name ?? 'N/A'); $sheet->setCellValue("C{$row}", $this->translateMovementType($movement->movement_type)); $sheet->setCellValue("D{$row}", $movement->warehouseFrom?->name ?? '-'); $sheet->setCellValue("E{$row}", $movement->warehouseTo?->name ?? '-'); $sheet->setCellValue("F{$row}", $movement->quantity); $sheet->setCellValue("G{$row}", $movement->unit_cost ? '$' . number_format($movement->unit_cost, 2) : ''); $sheet->setCellValue("H{$row}", $movement->invoice_reference ?? ''); $sheet->setCellValue("I{$row}", $movement->notes ?? ''); $sheet->setCellValue("J{$row}", $movement->user?->name ?? ''); // Estilo de fila $sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], 'font' => ['size' => 10], ]); // Color alterno if (($row - $headerRow) % 2 === 0) { $sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([ 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => 'D9E2F3']], ]); } $row++; } // ANCHOS DE COLUMNA $sheet->getColumnDimension('A')->setWidth(18); $sheet->getColumnDimension('B')->setWidth(30); $sheet->getColumnDimension('C')->setWidth(14); $sheet->getColumnDimension('D')->setWidth(18); $sheet->getColumnDimension('E')->setWidth(18); $sheet->getColumnDimension('F')->setWidth(12); $sheet->getColumnDimension('G')->setWidth(14); $sheet->getColumnDimension('H')->setWidth(18); $sheet->getColumnDimension('I')->setWidth(25); $sheet->getColumnDimension('J')->setWidth(16); // GUARDAR Y DESCARGAR $writer = new Xlsx($spreadsheet); $writer->save($filePath); return response()->download($filePath, $fileName)->deleteFileAfterSend(true); } }