- Implementa CRUD de unidades y soporte para decimales en inventario. - Integra servicios de WhatsApp para envío de documentos y auth. - Ajusta validación de series y permisos (RoleSeeder).
819 lines
37 KiB
PHP
819 lines
37 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\App;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Client;
|
|
use App\Models\Inventory;
|
|
use App\Models\Sale;
|
|
use App\Models\SaleDetail;
|
|
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 Carbon\Carbon;
|
|
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
|
|
|
class ExcelController extends Controller
|
|
{
|
|
/**
|
|
* Generar reporte Excel de descuentos a clientes
|
|
*/
|
|
public function clientDiscountsReport(Request $request)
|
|
{
|
|
// 1. VALIDACIÓN Y OBTENCIÓN DE DATOS
|
|
$request->validate([
|
|
'fecha_inicio' => 'required|date',
|
|
'fecha_fin' => 'required|date|after_or_equal:fecha_inicio',
|
|
'tier_id' => 'nullable|exists:client_tiers,id',
|
|
]);
|
|
|
|
$fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay();
|
|
$fechaFin = Carbon::parse($request->fecha_fin)->endOfDay();
|
|
$tierId = $request->tier_id;
|
|
|
|
// 2. OBTENER DATOS DE CLIENTES CON DESCUENTOS
|
|
$clients = Client::whereNotNull('tier_id')
|
|
->with('tier:id,tier_name,discount_percentage')
|
|
->whereHas('sales', function($q) use ($fechaInicio, $fechaFin) {
|
|
$q->whereBetween('created_at', [$fechaInicio, $fechaFin]);
|
|
})
|
|
->when($tierId, function($query) use ($tierId) {
|
|
$query->where('tier_id', $tierId);
|
|
})
|
|
->get();
|
|
|
|
if ($clients->isEmpty()) {
|
|
return response()->json(['message' => 'No se encontraron registros en el periodo especificado'], 404);
|
|
}
|
|
|
|
// 3. MAPEO DE DATOS
|
|
$data = $clients->map(function($client) use ($fechaInicio, $fechaFin) {
|
|
$sales = $client->sales()
|
|
->whereBetween('created_at', [$fechaInicio, $fechaFin])
|
|
->get();
|
|
|
|
return [
|
|
'numero' => $client->client_number,
|
|
'nombre' => $client->name,
|
|
'email' => $client->email ?? 'N/A',
|
|
'telefono' => $client->phone ?? 'N/A',
|
|
'tier' => $client->tier?->tier_name ?? 'N/A',
|
|
'descuento_porcentaje' => $client->tier?->discount_percentage ?? 0,
|
|
'total_compras' => $client->total_purchases,
|
|
'ventas_con_descuento' => $sales->count(),
|
|
'descuentos_recibidos' => $sales->sum('discount_amount'),
|
|
'promedio_descuento' => $sales->count() > 0 ? $sales->avg('discount_amount') : 0,
|
|
];
|
|
});
|
|
|
|
// 4. CONFIGURACIÓN EXCEL Y ESTILOS
|
|
$fileName = 'Reporte_Descuentos_Clientes_' . $fechaInicio->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();
|
|
|
|
// 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' => '4472C4']],
|
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true],
|
|
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]]
|
|
];
|
|
|
|
// --- ESTRUCTURA DEL DOCUMENTO ---
|
|
$sheet->getRowDimension(2)->setRowHeight(10);
|
|
$sheet->getRowDimension(3)->setRowHeight(25);
|
|
$sheet->getRowDimension(5)->setRowHeight(30);
|
|
|
|
// --- TÍTULO PRINCIPAL ---
|
|
$sheet->mergeCells('A3:J3');
|
|
$sheet->setCellValue('A3', 'REPORTE DE DESCUENTOS A CLIENTES');
|
|
$sheet->getStyle('A3')->applyFromArray([
|
|
'font' => ['bold' => true, 'size' => 16, 'color' => ['rgb' => '000000']],
|
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
|
]);
|
|
|
|
// --- INFORMACIÓN DEL PERIODO ---
|
|
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->mergeCells('A5:B5');
|
|
$sheet->setCellValue('A5', 'PERÍODO:');
|
|
$sheet->getStyle('A5')->applyFromArray($styleLabel);
|
|
|
|
$sheet->mergeCells('C5:J5');
|
|
$sheet->setCellValue('C5', $periodoTexto);
|
|
$sheet->getStyle('C5:J5')->applyFromArray($styleBox);
|
|
$sheet->getStyle('C5')->getFont()->setSize(12);
|
|
|
|
// --- RESUMEN DE TOTALES ---
|
|
$totalClientes = $data->count();
|
|
$totalVentas = $data->sum('ventas_con_descuento');
|
|
$totalDescuentos = $data->sum('descuentos_recibidos');
|
|
|
|
$row = 7;
|
|
$sheet->setCellValue('A' . $row, 'TOTAL CLIENTES CON DESCUENTOS:');
|
|
$sheet->setCellValueExplicit('C' . $row, (int) $totalClientes, DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValue('E' . $row, 'TOTAL VENTAS:');
|
|
$sheet->setCellValueExplicit('G' . $row, (int) $totalVentas, DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValue('I' . $row, 'TOTAL DESCUENTOS:');
|
|
$sheet->setCellValueExplicit('J' . $row, (float) $totalDescuentos, DataType::TYPE_NUMERIC);
|
|
|
|
// Aplicar formato moneda
|
|
$sheet->getStyle('G' . $row)->getNumberFormat()->setFormatCode('$#,##0.00');
|
|
$sheet->getStyle('J' . $row)->getNumberFormat()->setFormatCode('$#,##0.00');
|
|
|
|
$sheet->getStyle('A' . $row . ':J' . $row)->getFont()->setBold(true);
|
|
$sheet->getStyle('C' . $row)->getFont()->setSize(12)->getColor()->setRGB('4472C4');
|
|
$sheet->getStyle('G' . $row)->getFont()->setSize(12)->getColor()->setRGB('4472C4');
|
|
$sheet->getStyle('J' . $row)->getFont()->setSize(12)->getColor()->setRGB('008000');
|
|
|
|
// --- ENCABEZADOS DE TABLA ---
|
|
$h = 9;
|
|
$headers = [
|
|
'A' => 'No.',
|
|
'B' => "NÚMERO\nCLIENTE",
|
|
'C' => 'NOMBRE',
|
|
'D' => 'EMAIL',
|
|
'E' => 'TELÉFONO',
|
|
'F' => 'NIVEL',
|
|
'G' => "DESCUENTO\n(%)",
|
|
'H' => "VENTAS CON\nDESCUENTO",
|
|
'I' => "DESCUENTOS\nRECIBIDOS",
|
|
'J' => "PROMEDIO\nDESCUENTO"
|
|
];
|
|
|
|
foreach ($headers as $col => $text) {
|
|
$sheet->setCellValue("{$col}{$h}", $text);
|
|
}
|
|
|
|
$sheet->getStyle("A{$h}:J{$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['numero']);
|
|
$sheet->setCellValue('C' . $row, $item['nombre']);
|
|
$sheet->setCellValue('D' . $row, $item['email']);
|
|
$sheet->setCellValue('E' . $row, $item['telefono']);
|
|
$sheet->setCellValue('F' . $row, $item['tier']);
|
|
$sheet->setCellValue('G' . $row, $item['descuento_porcentaje'] . '%');
|
|
$sheet->setCellValue('H' . $row, $item['ventas_con_descuento']);
|
|
$sheet->setCellValue('I' . $row, '$' . number_format($item['descuentos_recibidos'], 2));
|
|
$sheet->setCellValue('J' . $row, '$' . number_format($item['promedio_descuento'], 2));
|
|
|
|
// Estilos de fila
|
|
$sheet->getStyle("A{$row}:J{$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("G{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
|
$sheet->getStyle("H{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
|
|
|
// Color alterno de filas
|
|
if ($i % 2 == 0) {
|
|
$sheet->getStyle("A{$row}:J{$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(25);
|
|
$sheet->getColumnDimension('D')->setWidth(25);
|
|
$sheet->getColumnDimension('E')->setWidth(15);
|
|
$sheet->getColumnDimension('F')->setWidth(12);
|
|
$sheet->getColumnDimension('G')->setWidth(12);
|
|
$sheet->getColumnDimension('H')->setWidth(15);
|
|
$sheet->getColumnDimension('I')->setWidth(18);
|
|
$sheet->getColumnDimension('J')->setWidth(18);
|
|
|
|
$writer = new Xlsx($spreadsheet);
|
|
$writer->save($filePath);
|
|
|
|
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
|
}
|
|
|
|
/**
|
|
* Generar reporte Excel de ventas
|
|
*/
|
|
public function salesReport(Request $request)
|
|
{
|
|
// 1. VALIDACIÓN
|
|
$request->validate([
|
|
'fecha_inicio' => 'required|date',
|
|
'fecha_fin' => 'required|date|after_or_equal:fecha_inicio',
|
|
'client_id' => 'nullable|exists:clients,id',
|
|
'user_id' => 'nullable|exists:users,id',
|
|
'status' => 'nullable|in:completed,cancelled',
|
|
]);
|
|
|
|
$fechaInicio = Carbon::parse($request->fecha_inicio)->startOfDay();
|
|
$fechaFin = Carbon::parse($request->fecha_fin)->endOfDay();
|
|
|
|
// 2. CONSULTA DE VENTAS
|
|
$sales = Sale::with(['client:id,name,client_number,rfc', 'user:id,name', 'details'])
|
|
->whereBetween('created_at', [$fechaInicio, $fechaFin])
|
|
->when($request->client_id, function($query) use ($request) {
|
|
$query->where('client_id', $request->client_id);
|
|
})
|
|
->when($request->user_id, function($query) use ($request) {
|
|
$query->where('user_id', $request->user_id);
|
|
})
|
|
->when($request->status, function($query) use ($request) {
|
|
$query->where('status', $request->status);
|
|
})
|
|
->orderBy('created_at', 'desc')
|
|
->get();
|
|
|
|
if ($sales->isEmpty()) {
|
|
return response()->json(['message' => 'No se encontraron ventas en el periodo especificado'], 404);
|
|
}
|
|
|
|
// 3. MAPEO DE DATOS
|
|
$data = $sales->map(function($sale) {
|
|
return [
|
|
'folio' => $sale->invoice_number,
|
|
'fecha' => $sale->created_at->format('d/m/Y H:i'),
|
|
'cliente' => $sale->client?->name ?? 'N/A',
|
|
'rfc_cliente' => $sale->client?->rfc ?? 'N/A',
|
|
'vendedor' => $sale->user?->name ?? 'N/A',
|
|
'subtotal' => (float) $sale->subtotal,
|
|
'iva' => (float) $sale->tax,
|
|
'descuento' => (float) ($sale->discount_amount ?? 0),
|
|
'total' => (float) $sale->total,
|
|
'metodo_pago' => $this->translatePaymentMethod($sale->payment_method),
|
|
'status' => $sale->status === 'completed' ? 'Completada' : 'Cancelada',
|
|
'productos' => $sale->details->count(),
|
|
];
|
|
});
|
|
|
|
// 4. CONFIGURACIÓN EXCEL
|
|
$fileName = 'Reporte_Ventas_' . $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();
|
|
|
|
// 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' => '2E75B6']],
|
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, 'wrapText' => true],
|
|
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]]
|
|
];
|
|
|
|
// --- ESTRUCTURA DEL DOCUMENTO ---
|
|
$lastCol = 'L';
|
|
$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 VENTAS');
|
|
$sheet->getStyle('A3')->applyFromArray([
|
|
'font' => ['bold' => true, 'size' => 16, 'color' => ['rgb' => '000000']],
|
|
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER],
|
|
]);
|
|
|
|
// --- INFORMACIÓN DEL PERIODO ---
|
|
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->mergeCells('A5:B5');
|
|
$sheet->setCellValue('A5', 'PERÍODO:');
|
|
$sheet->getStyle('A5')->applyFromArray($styleLabel);
|
|
|
|
$sheet->mergeCells("C5:{$lastCol}5");
|
|
$sheet->setCellValue('C5', $periodoTexto);
|
|
$sheet->getStyle("C5:{$lastCol}5")->applyFromArray($styleBox);
|
|
$sheet->getStyle('C5')->getFont()->setSize(12);
|
|
|
|
// --- RESUMEN DE TOTALES ---
|
|
$totalVentas = $data->count();
|
|
$totalSubtotal = $data->sum('subtotal');
|
|
$totalIva = $data->sum('iva');
|
|
$totalDescuentos = $data->sum('descuento');
|
|
$totalMonto = $data->sum('total');
|
|
|
|
$row = 7;
|
|
$sheet->setCellValue('A' . $row, 'TOTAL VENTAS:');
|
|
$sheet->setCellValue('B' . $row, $totalVentas);
|
|
$sheet->setCellValue('D' . $row, 'SUBTOTAL:');
|
|
$sheet->setCellValue('E' . $row, '$' . number_format($totalSubtotal, 2));
|
|
$sheet->setCellValue('G' . $row, 'IVA:');
|
|
$sheet->setCellValue('H' . $row, '$' . number_format($totalIva, 2));
|
|
$sheet->setCellValue('I' . $row, 'DESCUENTOS:');
|
|
$sheet->setCellValue('J' . $row, '$' . number_format($totalDescuentos, 2));
|
|
$sheet->setCellValue('K' . $row, 'TOTAL:');
|
|
$sheet->setCellValue('L' . $row, '$' . number_format($totalMonto, 2));
|
|
|
|
$sheet->getStyle("A{$row}:{$lastCol}{$row}")->getFont()->setBold(true);
|
|
$sheet->getStyle('B' . $row)->getFont()->setSize(12)->getColor()->setRGB('2E75B6');
|
|
$sheet->getStyle('E' . $row)->getFont()->setSize(12)->getColor()->setRGB('2E75B6');
|
|
$sheet->getStyle('H' . $row)->getFont()->setSize(12)->getColor()->setRGB('2E75B6');
|
|
$sheet->getStyle('J' . $row)->getFont()->setSize(12)->getColor()->setRGB('FF6600');
|
|
$sheet->getStyle('L' . $row)->getFont()->setSize(12)->getColor()->setRGB('008000');
|
|
|
|
// --- ENCABEZADOS DE TABLA ---
|
|
$h = 9;
|
|
$headers = [
|
|
'A' => 'No.',
|
|
'B' => 'FOLIO',
|
|
'C' => 'FECHA',
|
|
'D' => 'CLIENTE',
|
|
'E' => 'RFC',
|
|
'F' => 'VENDEDOR',
|
|
'G' => 'SUBTOTAL',
|
|
'H' => 'IVA',
|
|
'I' => 'DESCUENTO',
|
|
'J' => 'TOTAL',
|
|
'K' => "MÉTODO\nPAGO",
|
|
'L' => 'ESTADO',
|
|
];
|
|
|
|
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['folio']);
|
|
$sheet->setCellValue('C' . $row, $item['fecha']);
|
|
$sheet->setCellValue('D' . $row, $item['cliente']);
|
|
$sheet->setCellValue('E' . $row, $item['rfc_cliente']);
|
|
$sheet->setCellValue('F' . $row, $item['vendedor']);
|
|
$sheet->setCellValue('G' . $row, '$' . number_format($item['subtotal'], 2));
|
|
$sheet->setCellValue('H' . $row, '$' . number_format($item['iva'], 2));
|
|
$sheet->setCellValue('I' . $row, '$' . number_format($item['descuento'], 2));
|
|
$sheet->setCellValue('J' . $row, '$' . number_format($item['total'], 2));
|
|
$sheet->setCellValue('K' . $row, $item['metodo_pago']);
|
|
$sheet->setCellValue('L' . $row, $item['status']);
|
|
|
|
// 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("C{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
|
$sheet->getStyle("K{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
|
$sheet->getStyle("L{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
|
|
|
// Color de estado
|
|
if ($item['status'] === 'Cancelada') {
|
|
$sheet->getStyle("L{$row}")->getFont()->getColor()->setRGB('FF0000');
|
|
}
|
|
|
|
// 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(22);
|
|
$sheet->getColumnDimension('C')->setWidth(18);
|
|
$sheet->getColumnDimension('D')->setWidth(22);
|
|
$sheet->getColumnDimension('E')->setWidth(16);
|
|
$sheet->getColumnDimension('F')->setWidth(20);
|
|
$sheet->getColumnDimension('G')->setWidth(14);
|
|
$sheet->getColumnDimension('H')->setWidth(12);
|
|
$sheet->getColumnDimension('I')->setWidth(14);
|
|
$sheet->getColumnDimension('J')->setWidth(14);
|
|
$sheet->getColumnDimension('K')->setWidth(14);
|
|
$sheet->getColumnDimension('L')->setWidth(14);
|
|
|
|
$writer = new Xlsx($spreadsheet);
|
|
$writer->save($filePath);
|
|
|
|
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
|
}
|
|
|
|
/**
|
|
* Generar reporte Excel de inventario
|
|
*/
|
|
public function inventoryReport(Request $request)
|
|
{
|
|
// 1. VALIDACIÓN
|
|
$request->validate([
|
|
'fecha_inicio' => 'sometimes|date',
|
|
'fecha_fin' => 'sometimes|date|after_or_equal:fecha_inicio',
|
|
'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
|
|
$query = Inventory::with([
|
|
'category:id,name',
|
|
'price:inventory_id,cost,retail_price',
|
|
'unitOfMeasure:id,abbreviation',
|
|
'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) {
|
|
$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->whereHas('warehouses', function($wq) use ($request) {
|
|
$wq->where('inventory_warehouse.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) use ($fechaInicio, $fechaFin) {
|
|
// Cantidad vendida en el periodo
|
|
$quantitySold = SaleDetail::where('inventory_id', $inventory->id)
|
|
->whereHas('sale', function($q) use ($fechaInicio, $fechaFin) {
|
|
$q->where('status', 'completed')
|
|
->whereBetween('created_at', [$fechaInicio, $fechaFin]);
|
|
})
|
|
->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();
|
|
|
|
$cost = $inventory->price?->cost ?? 0;
|
|
$retailPrice = $inventory->price?->retail_price ?? 0;
|
|
$totalSold = $quantitySold * $retailPrice;
|
|
$inventoryValue = $inventory->stock * $cost;
|
|
$unitProfit = $retailPrice - $cost;
|
|
$totalProfit = $unitProfit * $quantitySold;
|
|
|
|
return [
|
|
'sku' => $inventory->sku ?? 'N/A',
|
|
'name' => $inventory->name,
|
|
'category' => $inventory->category?->name ?? 'Sin categoría',
|
|
'unit' => $inventory->unitOfMeasure?->abbreviation ?? 'u',
|
|
'stock' => $inventory->stock,
|
|
'quantity_sold' => $quantitySold,
|
|
'serials_total' => $serialsTotal,
|
|
'serials_available' => $serialsAvailable,
|
|
'serials_sold' => $serialsSold,
|
|
'serials_returned' => $serialsReturned,
|
|
'cost' => (float) $cost,
|
|
'price' => (float) $retailPrice,
|
|
'total_sold' => $totalSold,
|
|
'inventory_value' => $inventoryValue,
|
|
'unit_profit' => (float) $unitProfit,
|
|
'total_profit' => $totalProfit,
|
|
];
|
|
});
|
|
|
|
// 4. CONFIGURACIÓN EXCEL
|
|
$fileName = 'Reporte_Inventario_' . $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();
|
|
|
|
// 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 = 'N';
|
|
$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 DEL PERIODO ---
|
|
$sheet->mergeCells('A5:B5');
|
|
$sheet->setCellValue('A5', 'PERÍODO:');
|
|
$sheet->getStyle('A5')->applyFromArray($styleLabel);
|
|
|
|
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->mergeCells("C5:{$lastCol}5");
|
|
$sheet->setCellValue('C5', $periodoTexto);
|
|
$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');
|
|
$totalProfit = $data->sum('total_profit');
|
|
|
|
$row = 7;
|
|
// Columna A-B: TOTAL PRODUCTOS
|
|
$sheet->mergeCells("A{$row}:B{$row}");
|
|
$sheet->setCellValue("A{$row}", 'TOTAL PRODUCTOS:');
|
|
$sheet->setCellValueExplicit("C{$row}", (int) $totalProducts, DataType::TYPE_NUMERIC);
|
|
|
|
// Columna D-E: STOCK TOTAL
|
|
$sheet->mergeCells("D{$row}:E{$row}");
|
|
$sheet->setCellValue("D{$row}", 'STOCK TOTAL:');
|
|
$sheet->setCellValueExplicit("F{$row}", (int) $totalStock, DataType::TYPE_NUMERIC);
|
|
|
|
// Columna G: VENDIDOS
|
|
$sheet->setCellValue("G{$row}", 'VENDIDOS:');
|
|
$sheet->setCellValueExplicit("H{$row}", (int) $totalQuantitySold, DataType::TYPE_NUMERIC);
|
|
|
|
// Columna I-J: TOTAL VENDIDO
|
|
$sheet->mergeCells("I{$row}:J{$row}");
|
|
$sheet->setCellValue("I{$row}", 'TOTAL VENDIDO:');
|
|
$sheet->setCellValueExplicit("K{$row}", (float) $totalSoldValue, DataType::TYPE_NUMERIC);
|
|
|
|
// Columna L-M: VALOR INVENTARIO
|
|
$sheet->mergeCells("L{$row}:M{$row}");
|
|
$sheet->setCellValue("L{$row}", 'VALOR INVENTARIO:');
|
|
$sheet->setCellValueExplicit("N{$row}", (float) $totalInventoryValue, DataType::TYPE_NUMERIC);
|
|
|
|
// Columna O: UTILIDAD TOTAL
|
|
$sheet->setCellValue("O{$row}", 'UTILIDAD TOTAL:');
|
|
$sheet->setCellValueExplicit("P{$row}", (float) $totalProfit, DataType::TYPE_NUMERIC);
|
|
|
|
// Aplicar formato moneda
|
|
$sheet->getStyle("K{$row}")->getNumberFormat()->setFormatCode('$#,##0.00');
|
|
$sheet->getStyle("N{$row}")->getNumberFormat()->setFormatCode('$#,##0.00');
|
|
$sheet->getStyle("P{$row}")->getNumberFormat()->setFormatCode('$#,##0.00');
|
|
|
|
// Aplicar estilos a TODA la fila
|
|
$sheet->getStyle("A{$row}:P{$row}")->getFont()->setBold(true);
|
|
|
|
// Centrar vertical y horizontalmente TODOS los datos
|
|
$sheet->getStyle("A{$row}:P{$row}")->getAlignment()
|
|
->setHorizontal(Alignment::HORIZONTAL_CENTER)
|
|
->setVertical(Alignment::VERTICAL_CENTER);
|
|
|
|
// Colores para cada valor numérico
|
|
$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("N{$row}")->getFont()->setSize(12)->getColor()->setRGB('008000');
|
|
$sheet->getStyle("P{$row}")->getFont()->setSize(12);
|
|
|
|
// Color de utilidad (verde si es positiva, rojo si es negativa)
|
|
if ($totalProfit > 0) {
|
|
$sheet->getStyle("P{$row}")->getFont()->getColor()->setRGB('008000');
|
|
} elseif ($totalProfit < 0) {
|
|
$sheet->getStyle("P{$row}")->getFont()->getColor()->setRGB('FF0000');
|
|
}
|
|
|
|
// Altura de fila para que se vea mejor
|
|
$sheet->getRowDimension($row)->setRowHeight(25);
|
|
|
|
// --- ENCABEZADOS DE TABLA ---
|
|
$h = 9;
|
|
$headers = [
|
|
'A' => 'No.',
|
|
'B' => 'SKU',
|
|
'C' => 'NOMBRE',
|
|
'D' => 'CATEGORÍA',
|
|
'E' => 'UNIDAD',
|
|
'F' => "STOCK\nDISPONIBLE",
|
|
'G' => "CANTIDAD\nVENDIDA",
|
|
'H' => "SERIALES\nTOTALES",
|
|
'I' => "SERIALES\nDISPONIBLES",
|
|
'J' => "SERIALES\nVENDIDOS",
|
|
'K' => "SERIALES\nDEVUELTOS",
|
|
'L' => "COSTO\nUNITARIO",
|
|
'M' => "PRECIO\nVENTA",
|
|
'N' => "TOTAL\nVENDIDO",
|
|
'O' => "VALOR\nINVENTARIO",
|
|
'P' => "UTILIDAD\nPOR UNIDAD",
|
|
'Q' => "UTILIDAD\nTOTAL",
|
|
];
|
|
|
|
foreach ($headers as $col => $text) {
|
|
$sheet->setCellValue("{$col}{$h}", $text);
|
|
}
|
|
|
|
$sheet->getStyle("A{$h}:Q{$h}")->applyFromArray($styleTableHeader);
|
|
$sheet->getRowDimension($h)->setRowHeight(35);
|
|
|
|
// --- LLENADO DE DATOS ---
|
|
$row = 10;
|
|
$i = 1;
|
|
$totalProfit = 0;
|
|
|
|
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['unit']);
|
|
|
|
// NÚMEROS SIN FORMATO
|
|
$sheet->setCellValueExplicit('F' . $row, (float) $item['stock'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('G' . $row, (float) $item['quantity_sold'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('H' . $row, (int) $item['serials_total'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('I' . $row, (int) $item['serials_available'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('J' . $row, (int) $item['serials_sold'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('K' . $row, (int) $item['serials_returned'], DataType::TYPE_NUMERIC);
|
|
|
|
// NÚMEROS CON FORMATO MONEDA
|
|
$sheet->setCellValueExplicit('L' . $row, (float) $item['cost'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('M' . $row, (float) $item['price'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('N' . $row, (float) $item['total_sold'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('O' . $row, (float) $item['inventory_value'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('P' . $row, (float) $item['unit_profit'], DataType::TYPE_NUMERIC);
|
|
$sheet->setCellValueExplicit('Q' . $row, (float) $item['total_profit'], DataType::TYPE_NUMERIC);
|
|
|
|
$totalProfit += $item['total_profit'];
|
|
|
|
// Estilos de fila
|
|
$sheet->getStyle("A{$row}:Q{$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);
|
|
$sheet->getStyle("K{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
|
|
|
// Formato moneda para columnas L-Q
|
|
$sheet->getStyle("L{$row}:Q{$row}")->getNumberFormat()->setFormatCode('$#,##0.00');
|
|
|
|
// Color de utilidad (verde si es positiva)
|
|
if ($item['total_profit'] > 0) {
|
|
$sheet->getStyle("Q{$row}")->getFont()->getColor()->setRGB('008000');
|
|
} elseif ($item['total_profit'] < 0) {
|
|
$sheet->getStyle("Q{$row}")->getFont()->getColor()->setRGB('FF0000');
|
|
}
|
|
|
|
// Color alterno de filas
|
|
if ($i % 2 == 0) {
|
|
$sheet->getStyle("A{$row}:Q{$row}")->getFill()
|
|
->setFillType(Fill::FILL_SOLID)
|
|
->getStartColor()->setRGB('F2F2F2');
|
|
}
|
|
|
|
$row++;
|
|
$i++;
|
|
}
|
|
|
|
// --- ANCHOS DE COLUMNA ---
|
|
$sheet->getColumnDimension('A')->setWidth(6);
|
|
$sheet->getColumnDimension('B')->setWidth(15);
|
|
$sheet->getColumnDimension('C')->setWidth(25);
|
|
$sheet->getColumnDimension('D')->setWidth(18);
|
|
$sheet->getColumnDimension('E')->setWidth(10);
|
|
$sheet->getColumnDimension('F')->setWidth(15);
|
|
$sheet->getColumnDimension('G')->setWidth(15);
|
|
$sheet->getColumnDimension('H')->setWidth(15);
|
|
$sheet->getColumnDimension('I')->setWidth(15);
|
|
$sheet->getColumnDimension('J')->setWidth(15);
|
|
$sheet->getColumnDimension('K')->setWidth(15);
|
|
$sheet->getColumnDimension('L')->setWidth(15);
|
|
$sheet->getColumnDimension('M')->setWidth(15);
|
|
$sheet->getColumnDimension('N')->setWidth(18);
|
|
$sheet->getColumnDimension('O')->setWidth(18);
|
|
$sheet->getColumnDimension('P')->setWidth(18);
|
|
$sheet->getColumnDimension('Q')->setWidth(18);
|
|
|
|
$writer = new Xlsx($spreadsheet);
|
|
$writer->save($filePath);
|
|
|
|
return response()->download($filePath, $fileName)->deleteFileAfterSend(true);
|
|
}
|
|
|
|
/**
|
|
* Traducir método de pago
|
|
*/
|
|
private function translatePaymentMethod(?string $method): string
|
|
{
|
|
return match($method) {
|
|
'cash' => 'Efectivo',
|
|
'credit_card' => 'T. Crédito',
|
|
'debit_card' => 'T. Débito',
|
|
'transfer' => 'Transferencia',
|
|
default => $method ?? 'N/A',
|
|
};
|
|
}
|
|
}
|