feat: agregar controlador para generación de Kardex en Excel
This commit is contained in:
parent
9a78d92dbf
commit
516ad1cae6
@ -462,17 +462,29 @@ public function inventoryReport(Request $request)
|
|||||||
{
|
{
|
||||||
// 1. VALIDACIÓN
|
// 1. VALIDACIÓN
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'category_id' => 'nullable|exists:categories,id',
|
'fecha_inicio' => 'sometimes|date',
|
||||||
'with_serials_only' => 'nullable|boolean',
|
'fecha_fin' => 'sometimes|date|after_or_equal:fecha_inicio',
|
||||||
'low_stock_threshold' => 'nullable|integer|min:0',
|
'category_id' => 'sometimes|nullable|exists:categories,id',
|
||||||
|
'with_serials_only' => 'sometimes|nullable|boolean',
|
||||||
|
'low_stock_threshold' => 'sometimes|nullable|integer|min:0',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay();
|
||||||
|
$fechaFin = Carbon::parse($request->fecha_fin)->endOfDay();
|
||||||
|
|
||||||
// 2. CONSULTA DE INVENTARIO
|
// 2. CONSULTA DE INVENTARIO
|
||||||
$query = Inventory::with([
|
$query = Inventory::with([
|
||||||
'category:id,name',
|
'category:id,name',
|
||||||
'price:inventory_id,retail_price',
|
'price:inventory_id,cost,retail_price',
|
||||||
'serials'
|
'serials'
|
||||||
])
|
])
|
||||||
|
->when($request->q, function($q) use ($request) {
|
||||||
|
$q->where(function($query) use ($request) {
|
||||||
|
$query->where('name', 'like', "%{$request->q}%")
|
||||||
|
->orWhere('sku', 'like', "%{$request->q}%")
|
||||||
|
->orWhere('barcode', $request->q);
|
||||||
|
});
|
||||||
|
})
|
||||||
->when($request->category_id, function($q) use ($request) {
|
->when($request->category_id, function($q) use ($request) {
|
||||||
$q->where('category_id', $request->category_id);
|
$q->where('category_id', $request->category_id);
|
||||||
})
|
})
|
||||||
@ -492,11 +504,12 @@ public function inventoryReport(Request $request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. MAPEO DE DATOS
|
// 3. MAPEO DE DATOS
|
||||||
$data = $inventories->map(function($inventory) {
|
$data = $inventories->map(function($inventory) use ($fechaInicio, $fechaFin) {
|
||||||
// Cantidad vendida total
|
// Cantidad vendida en el periodo
|
||||||
$quantitySold = SaleDetail::where('inventory_id', $inventory->id)
|
$quantitySold = SaleDetail::where('inventory_id', $inventory->id)
|
||||||
->whereHas('sale', function($q) {
|
->whereHas('sale', function($q) use ($fechaInicio, $fechaFin) {
|
||||||
$q->where('status', 'completed');
|
$q->where('status', 'completed')
|
||||||
|
->whereBetween('created_at', [$fechaInicio, $fechaFin]);
|
||||||
})
|
})
|
||||||
->sum('quantity');
|
->sum('quantity');
|
||||||
|
|
||||||
@ -506,9 +519,10 @@ public function inventoryReport(Request $request)
|
|||||||
$serialsSold = $inventory->serials->where('status', 'vendido')->count();
|
$serialsSold = $inventory->serials->where('status', 'vendido')->count();
|
||||||
$serialsReturned = $inventory->serials->where('status', 'devuelto')->count();
|
$serialsReturned = $inventory->serials->where('status', 'devuelto')->count();
|
||||||
|
|
||||||
$price = $inventory->price?->retail_price ?? 0;
|
$cost = $inventory->price?->cost ?? 0;
|
||||||
$totalSold = $quantitySold * $price;
|
$retailPrice = $inventory->price?->retail_price ?? 0;
|
||||||
$inventoryValue = $inventory->stock * $price;
|
$totalSold = $quantitySold * $retailPrice;
|
||||||
|
$inventoryValue = $inventory->stock * $cost;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'sku' => $inventory->sku ?? 'N/A',
|
'sku' => $inventory->sku ?? 'N/A',
|
||||||
@ -520,14 +534,15 @@ public function inventoryReport(Request $request)
|
|||||||
'serials_available' => $serialsAvailable,
|
'serials_available' => $serialsAvailable,
|
||||||
'serials_sold' => $serialsSold,
|
'serials_sold' => $serialsSold,
|
||||||
'serials_returned' => $serialsReturned,
|
'serials_returned' => $serialsReturned,
|
||||||
'price' => (float) $price,
|
'cost' => (float) $cost,
|
||||||
|
'price' => (float) $retailPrice,
|
||||||
'total_sold' => $totalSold,
|
'total_sold' => $totalSold,
|
||||||
'inventory_value' => $inventoryValue,
|
'inventory_value' => $inventoryValue,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. CONFIGURACIÓN EXCEL
|
// 4. CONFIGURACIÓN EXCEL
|
||||||
$fileName = 'Reporte_Inventario_' . now()->format('Ymd_His') . '.xlsx';
|
$fileName = 'Reporte_Inventario_' . $fechaInicio->format('Ymd') . '_' . $fechaFin->format('Ymd') . '.xlsx';
|
||||||
$filePath = storage_path('app/temp/' . $fileName);
|
$filePath = storage_path('app/temp/' . $fileName);
|
||||||
if (!file_exists(dirname($filePath))) mkdir(dirname($filePath), 0755, true);
|
if (!file_exists(dirname($filePath))) mkdir(dirname($filePath), 0755, true);
|
||||||
|
|
||||||
@ -555,7 +570,7 @@ public function inventoryReport(Request $request)
|
|||||||
];
|
];
|
||||||
|
|
||||||
// --- ESTRUCTURA DEL DOCUMENTO ---
|
// --- ESTRUCTURA DEL DOCUMENTO ---
|
||||||
$lastCol = 'M';
|
$lastCol = 'N';
|
||||||
$sheet->getRowDimension(2)->setRowHeight(10);
|
$sheet->getRowDimension(2)->setRowHeight(10);
|
||||||
$sheet->getRowDimension(3)->setRowHeight(25);
|
$sheet->getRowDimension(3)->setRowHeight(25);
|
||||||
$sheet->getRowDimension(5)->setRowHeight(30);
|
$sheet->getRowDimension(5)->setRowHeight(30);
|
||||||
@ -568,14 +583,20 @@ public function inventoryReport(Request $request)
|
|||||||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// --- INFORMACIÓN DE FECHA ---
|
// --- INFORMACIÓN DEL PERIODO ---
|
||||||
$sheet->mergeCells('A5:B5');
|
$sheet->mergeCells('A5:B5');
|
||||||
$sheet->setCellValue('A5', 'FECHA:');
|
$sheet->setCellValue('A5', 'PERÍODO:');
|
||||||
$sheet->getStyle('A5')->applyFromArray($styleLabel);
|
$sheet->getStyle('A5')->applyFromArray($styleLabel);
|
||||||
|
|
||||||
$sheet->mergeCells("C5:{$lastCol}5");
|
|
||||||
Carbon::setLocale('es');
|
Carbon::setLocale('es');
|
||||||
$sheet->setCellValue('C5', ucfirst(now()->translatedFormat('l j \d\e F Y')));
|
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->mergeCells("C5:{$lastCol}5");
|
||||||
|
$sheet->setCellValue('C5', $periodoTexto);
|
||||||
$sheet->getStyle("C5:{$lastCol}5")->applyFromArray($styleBox);
|
$sheet->getStyle("C5:{$lastCol}5")->applyFromArray($styleBox);
|
||||||
$sheet->getStyle('C5')->getFont()->setSize(12);
|
$sheet->getStyle('C5')->getFont()->setSize(12);
|
||||||
|
|
||||||
@ -618,9 +639,10 @@ public function inventoryReport(Request $request)
|
|||||||
'H' => "SERIALES\nDISPONIBLES",
|
'H' => "SERIALES\nDISPONIBLES",
|
||||||
'I' => "SERIALES\nVENDIDOS",
|
'I' => "SERIALES\nVENDIDOS",
|
||||||
'J' => "SERIALES\nDEVUELTOS",
|
'J' => "SERIALES\nDEVUELTOS",
|
||||||
'K' => "PRECIO\nUNITARIO",
|
'K' => "COSTO\nUNITARIO",
|
||||||
'L' => "TOTAL\nVENDIDO",
|
'L' => "PRECIO\nVENTA",
|
||||||
'M' => "VALOR\nINVENTARIO",
|
'M' => "TOTAL\nVENDIDO",
|
||||||
|
'N' => "VALOR\nINVENTARIO",
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($headers as $col => $text) {
|
foreach ($headers as $col => $text) {
|
||||||
@ -645,9 +667,10 @@ public function inventoryReport(Request $request)
|
|||||||
$sheet->setCellValue('H' . $row, $item['serials_available']);
|
$sheet->setCellValue('H' . $row, $item['serials_available']);
|
||||||
$sheet->setCellValue('I' . $row, $item['serials_sold']);
|
$sheet->setCellValue('I' . $row, $item['serials_sold']);
|
||||||
$sheet->setCellValue('J' . $row, $item['serials_returned']);
|
$sheet->setCellValue('J' . $row, $item['serials_returned']);
|
||||||
$sheet->setCellValue('K' . $row, '$' . number_format($item['price'], 2));
|
$sheet->setCellValue('K' . $row, '$' . number_format($item['cost'], 2));
|
||||||
$sheet->setCellValue('L' . $row, '$' . number_format($item['total_sold'], 2));
|
$sheet->setCellValue('L' . $row, '$' . number_format($item['price'], 2));
|
||||||
$sheet->setCellValue('M' . $row, '$' . number_format($item['inventory_value'], 2));
|
$sheet->setCellValue('M' . $row, '$' . number_format($item['total_sold'], 2));
|
||||||
|
$sheet->setCellValue('N' . $row, '$' . number_format($item['inventory_value'], 2));
|
||||||
|
|
||||||
// Estilos de fila
|
// Estilos de fila
|
||||||
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->applyFromArray([
|
||||||
@ -694,9 +717,10 @@ public function inventoryReport(Request $request)
|
|||||||
$sheet->getColumnDimension('H')->setWidth(14);
|
$sheet->getColumnDimension('H')->setWidth(14);
|
||||||
$sheet->getColumnDimension('I')->setWidth(12);
|
$sheet->getColumnDimension('I')->setWidth(12);
|
||||||
$sheet->getColumnDimension('J')->setWidth(12);
|
$sheet->getColumnDimension('J')->setWidth(12);
|
||||||
$sheet->getColumnDimension('K')->setWidth(18);
|
$sheet->getColumnDimension('K')->setWidth(14);
|
||||||
$sheet->getColumnDimension('L')->setWidth(14);
|
$sheet->getColumnDimension('L')->setWidth(14);
|
||||||
$sheet->getColumnDimension('M')->setWidth(16);
|
$sheet->getColumnDimension('M')->setWidth(14);
|
||||||
|
$sheet->getColumnDimension('N')->setWidth(16);
|
||||||
|
|
||||||
$writer = new Xlsx($spreadsheet);
|
$writer = new Xlsx($spreadsheet);
|
||||||
$writer->save($filePath);
|
$writer->save($filePath);
|
||||||
|
|||||||
@ -11,9 +11,6 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Controlador para movimientos de inventario
|
* Controlador para movimientos de inventario
|
||||||
*
|
|
||||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
|
||||||
* @version 1.0.0
|
|
||||||
*/
|
*/
|
||||||
class InventoryMovementController extends Controller
|
class InventoryMovementController extends Controller
|
||||||
{
|
{
|
||||||
|
|||||||
361
app/Http/Controllers/App/KardexController.php
Normal file
361
app/Http/Controllers/App/KardexController.php
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
<?php namespace App\Http\Controllers\App;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Inventory;
|
||||||
|
use App\Models\InventoryMovement;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Color;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controlador para generación de Kardex (Excel)
|
||||||
|
*/
|
||||||
|
class KardexController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generar Kardex en Excel para un producto
|
||||||
|
*/
|
||||||
|
public function export(Request $request)
|
||||||
|
{
|
||||||
|
// 1. VALIDACIÓN
|
||||||
|
$request->validate([
|
||||||
|
'inventory_id' => 'required|exists:inventories,id',
|
||||||
|
'warehouse_id' => 'sometimes|nullable|exists:warehouses,id',
|
||||||
|
'fecha_inicio' => 'required|date',
|
||||||
|
'fecha_fin' => 'required|date|after_or_equal:fecha_inicio',
|
||||||
|
'movement_type' => 'sometimes|nullable|in:entry,exit,transfer,sale,return',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay();
|
||||||
|
$fechaFin = Carbon::parse($request->fecha_fin)->endOfDay();
|
||||||
|
|
||||||
|
$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. CALCULAR SALDO INICIAL (movimientos previos al rango)
|
||||||
|
$initialBalance = $this->calculateInitialBalance($inventory->id, $fechaInicio, $warehouseId);
|
||||||
|
|
||||||
|
// 4. CONSTRUIR LÍNEAS DEL KARDEX
|
||||||
|
$balance = $initialBalance;
|
||||||
|
$kardexLines = [];
|
||||||
|
|
||||||
|
foreach ($movements as $movement) {
|
||||||
|
$effect = $this->calculateMovementEffect($movement, $warehouseId);
|
||||||
|
$entryQty = $effect > 0 ? $effect : 0;
|
||||||
|
$exitQty = $effect < 0 ? abs($effect) : 0;
|
||||||
|
$balance += $effect;
|
||||||
|
|
||||||
|
$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 ?? '-',
|
||||||
|
'entry' => $entryQty,
|
||||||
|
'exit' => $exitQty,
|
||||||
|
'balance' => $balance,
|
||||||
|
'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 = 'K';
|
||||||
|
|
||||||
|
// --- 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);
|
||||||
|
|
||||||
|
// --- SALDO INICIAL ---
|
||||||
|
$row++;
|
||||||
|
$sheet->setCellValue("A{$row}", 'SALDO INICIAL:');
|
||||||
|
$sheet->getStyle("A{$row}")->applyFromArray($styleLabel);
|
||||||
|
$sheet->mergeCells("B{$row}:C{$row}");
|
||||||
|
$sheet->setCellValue("B{$row}", $initialBalance);
|
||||||
|
$sheet->getStyle("B{$row}:C{$row}")->applyFromArray($styleBox);
|
||||||
|
$sheet->getStyle("B{$row}")->getFont()->setBold(true);
|
||||||
|
|
||||||
|
// --- ENCABEZADOS DE TABLA ---
|
||||||
|
$row += 2;
|
||||||
|
$headerRow = $row;
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'A' => 'FECHA',
|
||||||
|
'B' => 'TIPO',
|
||||||
|
'C' => "ALMACÉN\nORIGEN",
|
||||||
|
'D' => "ALMACÉN\nDESTINO",
|
||||||
|
'E' => 'ENTRADA',
|
||||||
|
'F' => 'SALIDA',
|
||||||
|
'G' => 'SALDO',
|
||||||
|
'H' => "COSTO\nUNITARIO",
|
||||||
|
'I' => 'REFERENCIA',
|
||||||
|
'J' => 'NOTAS',
|
||||||
|
'K' => '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++;
|
||||||
|
$totalEntries = 0;
|
||||||
|
$totalExits = 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['entry'] ?: '');
|
||||||
|
$sheet->setCellValue("F{$row}", $line['exit'] ?: '');
|
||||||
|
$sheet->setCellValue("G{$row}", $line['balance']);
|
||||||
|
$sheet->setCellValue("H{$row}", $line['unit_cost'] ? '$' . number_format($line['unit_cost'], 2) : '');
|
||||||
|
$sheet->setCellValue("I{$row}", $line['invoice_reference']);
|
||||||
|
$sheet->setCellValue("J{$row}", $line['notes']);
|
||||||
|
$sheet->setCellValue("K{$row}", $line['user']);
|
||||||
|
|
||||||
|
$totalEntries += $line['entry'];
|
||||||
|
$totalExits += $line['exit'];
|
||||||
|
|
||||||
|
// 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']],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resaltar entradas en verde y salidas en rojo
|
||||||
|
if ($line['entry'] > 0) {
|
||||||
|
$sheet->getStyle("E{$row}")->getFont()->setColor(new Color('006100'));
|
||||||
|
$sheet->getStyle("E{$row}")->getFont()->setBold(true);
|
||||||
|
}
|
||||||
|
if ($line['exit'] > 0) {
|
||||||
|
$sheet->getStyle("F{$row}")->getFont()->setColor(new Color('9C0006'));
|
||||||
|
$sheet->getStyle("F{$row}")->getFont()->setBold(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FILA DE TOTALES ---
|
||||||
|
$sheet->mergeCells("A{$row}:D{$row}");
|
||||||
|
$sheet->setCellValue("A{$row}", 'TOTALES');
|
||||||
|
$sheet->setCellValue("E{$row}", $totalEntries);
|
||||||
|
$sheet->setCellValue("F{$row}", $totalExits);
|
||||||
|
$sheet->setCellValue("G{$row}", $balance);
|
||||||
|
|
||||||
|
$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(12);
|
||||||
|
$sheet->getColumnDimension('G')->setWidth(12);
|
||||||
|
$sheet->getColumnDimension('H')->setWidth(14);
|
||||||
|
$sheet->getColumnDimension('I')->setWidth(18);
|
||||||
|
$sheet->getColumnDimension('J')->setWidth(25);
|
||||||
|
$sheet->getColumnDimension('K')->setWidth(16);
|
||||||
|
|
||||||
|
// 6. GUARDAR Y DESCARGAR
|
||||||
|
$writer = new Xlsx($spreadsheet);
|
||||||
|
$writer->save($filePath);
|
||||||
|
|
||||||
|
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcular saldo inicial (movimientos previos al rango de fechas)
|
||||||
|
*/
|
||||||
|
private function calculateInitialBalance(int $inventoryId, Carbon $beforeDate, ?int $warehouseId): int
|
||||||
|
{
|
||||||
|
$query = InventoryMovement::where('inventory_id', $inventoryId)
|
||||||
|
->where('created_at', '<', $beforeDate)
|
||||||
|
->orderBy('created_at', 'asc')
|
||||||
|
->orderBy('id', 'asc');
|
||||||
|
|
||||||
|
if ($warehouseId) {
|
||||||
|
$query->where(function ($q) use ($warehouseId) {
|
||||||
|
$q->where('warehouse_from_id', $warehouseId)
|
||||||
|
->orWhere('warehouse_to_id', $warehouseId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$balance = 0;
|
||||||
|
foreach ($query->get() as $movement) {
|
||||||
|
$balance += $this->calculateMovementEffect($movement, $warehouseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcular el efecto de un movimiento en el stock
|
||||||
|
*/
|
||||||
|
private function calculateMovementEffect(InventoryMovement $movement, ?int $warehouseId): int
|
||||||
|
{
|
||||||
|
// Sin filtro de almacén: stock global
|
||||||
|
if (!$warehouseId) {
|
||||||
|
return match ($movement->movement_type) {
|
||||||
|
'entry', 'return' => $movement->quantity,
|
||||||
|
'exit', 'sale' => -$movement->quantity,
|
||||||
|
'transfer' => 0,
|
||||||
|
default => 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Con filtro de almacén: depende de la dirección
|
||||||
|
if ($movement->warehouse_to_id == $warehouseId && $movement->warehouse_from_id != $warehouseId) {
|
||||||
|
return $movement->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($movement->warehouse_from_id == $warehouseId && $movement->warehouse_to_id != $warehouseId) {
|
||||||
|
return -$movement->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@
|
|||||||
use App\Http\Controllers\App\InventorySerialController;
|
use App\Http\Controllers\App\InventorySerialController;
|
||||||
use App\Http\Controllers\App\InvoiceRequestController;
|
use App\Http\Controllers\App\InvoiceRequestController;
|
||||||
use App\Http\Controllers\App\InventoryMovementController;
|
use App\Http\Controllers\App\InventoryMovementController;
|
||||||
|
use App\Http\Controllers\App\KardexController;
|
||||||
use App\Http\Controllers\App\WarehouseController;
|
use App\Http\Controllers\App\WarehouseController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@ -94,6 +95,7 @@
|
|||||||
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']);
|
Route::get('inventory/excel', [ExcelController::class, 'inventoryReport']);
|
||||||
|
Route::get('kardex/excel', [KardexController::class, 'export']);
|
||||||
});
|
});
|
||||||
|
|
||||||
//CLIENTES
|
//CLIENTES
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user