Compare commits
No commits in common. "main" and "develop" have entirely different histories.
@ -1,114 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Exports;
|
|
||||||
|
|
||||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
|
||||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
|
||||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
|
||||||
use Maatwebsite\Excel\Concerns\WithTitle;
|
|
||||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
|
||||||
use App\Models\SaleItem;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class CashCloseReportExport implements FromCollection, WithHeadings, WithStyles, WithTitle
|
|
||||||
{
|
|
||||||
protected $cashCloses;
|
|
||||||
protected $cashCloseIds;
|
|
||||||
|
|
||||||
public function __construct($cashCloses)
|
|
||||||
{
|
|
||||||
$this->cashCloses = $cashCloses;
|
|
||||||
$this->cashCloseIds = $cashCloses->pluck('id')->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function collection()
|
|
||||||
{
|
|
||||||
$data = collect();
|
|
||||||
|
|
||||||
// RESUMEN FINANCIERO
|
|
||||||
$totalIncome = $this->cashCloses->sum('income');
|
|
||||||
$totalExit = $this->cashCloses->sum('exit');
|
|
||||||
$totalCash = $this->cashCloses->sum('income_cash');
|
|
||||||
$totalCard = $this->cashCloses->sum('income_card');
|
|
||||||
$totalTransfer = $this->cashCloses->sum('income_transfer');
|
|
||||||
$balanceFinal = $this->cashCloses->sum('initial_balance') + $totalIncome - $totalExit;
|
|
||||||
|
|
||||||
$data->push(['RESUMEN FINANCIERO', '', '', '', '', '']);
|
|
||||||
$data->push(['Periodo Inicio', $this->cashCloses->last()?->opened_at, '', '', '', '']);
|
|
||||||
$data->push(['Periodo Fin', $this->cashCloses->first()?->closed_at, '', '', '', '']);
|
|
||||||
$data->push(['Total Ventas', $totalIncome, '', '', '', '']);
|
|
||||||
$data->push(['Efectivo', $totalCash, '', '', '', '']);
|
|
||||||
$data->push(['Tarjeta', $totalCard, '', '', '', '']);
|
|
||||||
$data->push(['Transferencia', $totalTransfer, '', '', '', '']);
|
|
||||||
$data->push(['Egresos', $totalExit, '', '', '', '']);
|
|
||||||
$data->push(['Balance Final', $balanceFinal, '', '', '', '']);
|
|
||||||
$data->push(['', '', '', '', '', '']); // Espacios
|
|
||||||
|
|
||||||
// ESTADÍSTICAS POR PAQUETE
|
|
||||||
$packageStats = DB::table('sale_items')
|
|
||||||
->join('sales', 'sale_items.sale_id', '=', 'sales.id')
|
|
||||||
->join('packages', 'sale_items.package_id', '=', 'packages.id')
|
|
||||||
->whereIn('sales.cash_close_id', $this->cashCloseIds)
|
|
||||||
->select(
|
|
||||||
'packages.name as paquete',
|
|
||||||
DB::raw('COUNT(*) as total_vendidos'),
|
|
||||||
DB::raw('SUM(packages.price) as total_ingresos')
|
|
||||||
)
|
|
||||||
->groupBy('packages.id', 'packages.name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$data->push(['VENTAS POR PAQUETE', '', '', '', '', '']);
|
|
||||||
$data->push(['Paquete', 'Total Vendidos', 'Total Ingresos', '', '', '']);
|
|
||||||
foreach ($packageStats as $stat) {
|
|
||||||
$data->push([$stat->paquete, $stat->total_vendidos, $stat->total_ingresos, '', '', '']);
|
|
||||||
}
|
|
||||||
$data->push(['', '', '', '', '', '']); // Espacios
|
|
||||||
|
|
||||||
// VENTAS DETALLADAS
|
|
||||||
$detailedSales = SaleItem::whereHas('sale', function ($query) {
|
|
||||||
$query->whereIn('cash_close_id', $this->cashCloseIds);
|
|
||||||
})
|
|
||||||
->with([
|
|
||||||
'sale.client:id,name,paternal,maternal',
|
|
||||||
'sale:id,client_id,payment_method',
|
|
||||||
'simCard:id,iccid,msisdn',
|
|
||||||
'package:id,name,price'
|
|
||||||
])
|
|
||||||
->orderBy('id', 'asc')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$data->push(['VENTAS DETALLADAS', '', '', '', '', '']);
|
|
||||||
$data->push(['Nombre Comprador', 'ID SIM', 'Número Asignado', 'Paquete', 'Costo', 'Medio de Pago']);
|
|
||||||
|
|
||||||
foreach ($detailedSales as $item) {
|
|
||||||
$data->push([
|
|
||||||
$item->sale->client->full_name,
|
|
||||||
$item->simCard->iccid,
|
|
||||||
$item->simCard->msisdn,
|
|
||||||
$item->package->name,
|
|
||||||
$item->package->price,
|
|
||||||
ucfirst($item->sale->payment_method)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function headings(): array
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function title(): string
|
|
||||||
{
|
|
||||||
return 'Reporte Corte de Caja';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function styles(Worksheet $sheet)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
1 => ['font' => ['bold' => true, 'size' => 14]],
|
|
||||||
11 => ['font' => ['bold' => true, 'size' => 14]],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,8 +9,7 @@
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
use App\Exports\CashCloseReportExport;
|
use phpseclib3\Crypt\RC2;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -38,6 +37,62 @@ public function index(Request $request)
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function closeCashClose(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'exit' => 'sometimes|numeric|min:0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cashClose = CashClose::open()->first();
|
||||||
|
|
||||||
|
if (!$cashClose) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'No hay un corte de caja abierto para cerrar.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalSales = Sale::where('cash_close_id', $cashClose->id)->sum('total_amount');
|
||||||
|
|
||||||
|
$paymentMethods = Sale::where('cash_close_id', $cashClose->id)
|
||||||
|
->select('payment_method', DB::raw('SUM(total_amount) as total'))
|
||||||
|
->groupBy('payment_method')
|
||||||
|
->pluck('total', 'payment_method');
|
||||||
|
|
||||||
|
$exit = $request->input('exit', 0);
|
||||||
|
|
||||||
|
$cashClose->update([
|
||||||
|
'closed_at' => now(),
|
||||||
|
'income' => $totalSales,
|
||||||
|
'exit' => $exit,
|
||||||
|
'income_cash' => $paymentMethods->get('cash', 0),
|
||||||
|
'income_card' => $paymentMethods->get('card', 0),
|
||||||
|
'income_transfer' => $paymentMethods->get('transfer', 0),
|
||||||
|
'status' => 'closed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$balanceFinal = $cashClose->initial_balance + $totalSales - $exit;
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Corte de caja cerrado exitosamente.',
|
||||||
|
'cash_close' => $cashClose->fresh('user'),
|
||||||
|
'resumen' => [
|
||||||
|
'periodo' => [
|
||||||
|
'apertura' => $cashClose->opened_at,
|
||||||
|
'cierre' => $cashClose->closed_at,
|
||||||
|
],
|
||||||
|
'totales' => [
|
||||||
|
'fondo_inicial' => $cashClose->initial_balance,
|
||||||
|
'total_ventas' => $totalSales,
|
||||||
|
'efectivo' => $paymentMethods->get('cash', 0),
|
||||||
|
'tarjeta' => $paymentMethods->get('card', 0),
|
||||||
|
'transferencia' => $paymentMethods->get('transfer', 0),
|
||||||
|
'egresos' => $exit,
|
||||||
|
'balance_final' => $balanceFinal,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function report(Request $request)
|
public function report(Request $request)
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
@ -155,11 +210,11 @@ public function exportReport(Request $request)
|
|||||||
$query = CashClose::with('user:id,name')->withCount('sales');
|
$query = CashClose::with('user:id,name')->withCount('sales');
|
||||||
|
|
||||||
if ($request->has('start_date') && $request->has('end_date')) {
|
if ($request->has('start_date') && $request->has('end_date')) {
|
||||||
$query->whereBetween('closed_at', [$request->start_date, $request->end_date]);
|
$query->whereBetween('close_at', [$request->start_date, $request->end_date]);
|
||||||
} elseif ($request->has('start_date')) {
|
} elseif ($request->has('start_date')) {
|
||||||
$query->whereDate('closed_at', '>=', $request->start_date);
|
$query->whereDate('close_at', '>=', $request->start_date);
|
||||||
} elseif ($request->has('end_date')) {
|
} elseif ($request->has('end_date')) {
|
||||||
$query->whereDate('closed_at', '<=', $request->end_date);
|
$query->whereDate('close_at', '<=', $request->end_date);
|
||||||
} else {
|
} else {
|
||||||
$query->closed()->orderBy('id', 'desc')->limit(1);
|
$query->closed()->orderBy('id', 'desc')->limit(1);
|
||||||
}
|
}
|
||||||
@ -172,232 +227,70 @@ public function exportReport(Request $request)
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$filename = 'reporte_corte_caja_' . date('Y-m-d_His') . '.xlsx';
|
$cashCloseIds = $cashCloses->pluck('id')->toArray();
|
||||||
|
|
||||||
return Excel::download(
|
// Obtener ventas detalladas
|
||||||
new CashCloseReportExport($cashCloses),
|
$detailedSales = SaleItem::whereHas('sale', function ($query) use ($cashCloseIds) {
|
||||||
$filename
|
$query->whereIn('cash_close_id', $cashCloseIds);
|
||||||
);
|
})
|
||||||
}
|
->with([
|
||||||
|
'sale.client:id,name,paternal,maternal',
|
||||||
/**
|
'sale:id,client_id,payment_method',
|
||||||
* Preview de ventas disponibles para crear corte por rango de fechas
|
'simCard:id,iccid,msisdn',
|
||||||
*/
|
'package:id,name,price'
|
||||||
public function previewSalesRange(Request $request)
|
])
|
||||||
{
|
->orderBy('id', 'asc')
|
||||||
$request->validate([
|
|
||||||
'start_date' => 'required|date',
|
|
||||||
'end_date' => 'required|date|after_or_equal:start_date',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Buscar ventas sin corte en el rango especificado
|
|
||||||
$availableSales = Sale::with([
|
|
||||||
'client:id,name,paternal,maternal',
|
|
||||||
'saleItems.simCard:id,iccid,msisdn',
|
|
||||||
'saleItems.package:id,name,price'
|
|
||||||
])
|
|
||||||
->whereNull('cash_close_id')
|
|
||||||
->whereBetween('sale_date', [$request->start_date, $request->end_date])
|
|
||||||
->orderBy('sale_date', 'asc')
|
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
if ($availableSales->isEmpty()) {
|
// Calcular totales
|
||||||
return ApiResponse::NOT_FOUND->response([
|
$totalIncome = $cashCloses->sum('income');
|
||||||
'message' => 'No se encontraron ventas disponibles en el rango de fechas especificado.',
|
$totalExit = $cashCloses->sum('exit');
|
||||||
'total_sales' => 0,
|
$totalCash = $cashCloses->sum('income_cash');
|
||||||
]);
|
$totalCard = $cashCloses->sum('income_card');
|
||||||
}
|
$totalTransfer = $cashCloses->sum('income_transfer');
|
||||||
|
|
||||||
// Calcular totales por método de pago
|
// Crear el CSV
|
||||||
$paymentMethods = $availableSales->groupBy('payment_method')->map(function ($sales, $method) {
|
$filename = 'reporte_corte_caja_' . date('Y-m-d_His') . '.csv';
|
||||||
return [
|
|
||||||
'method' => $method,
|
|
||||||
'count' => $sales->count(),
|
|
||||||
'total' => $sales->sum('total_amount'),
|
|
||||||
];
|
|
||||||
})->values();
|
|
||||||
|
|
||||||
$totalAmount = $availableSales->sum('total_amount');
|
$headers = [
|
||||||
|
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||||
|
];
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
$callback = function () use ($cashCloses, $detailedSales, $totalIncome, $totalExit, $totalCash, $totalCard, $totalTransfer) {
|
||||||
'message' => 'Ventas disponibles encontradas',
|
$file = fopen('php://output', 'w');
|
||||||
'periodo' => [
|
|
||||||
'start_date' => $request->start_date,
|
|
||||||
'end_date' => $request->end_date,
|
|
||||||
],
|
|
||||||
'resumen' => [
|
|
||||||
'total_ventas' => $availableSales->count(),
|
|
||||||
'monto_total' => $totalAmount,
|
|
||||||
'por_metodo_pago' => $paymentMethods,
|
|
||||||
],
|
|
||||||
'ventas' => $availableSales->map(function ($sale) {
|
|
||||||
return [
|
|
||||||
'id' => $sale->id,
|
|
||||||
'sale_date' => $sale->sale_date,
|
|
||||||
'client' => $sale->client->full_name ?? 'Sin cliente',
|
|
||||||
'payment_method' => $sale->payment_method,
|
|
||||||
'total_amount' => $sale->total_amount,
|
|
||||||
'items_count' => $sale->saleItems->count(),
|
|
||||||
];
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||||
* Crear corte de caja desde un rango de fechas
|
|
||||||
*/
|
|
||||||
public function createFromRange(Request $request)
|
|
||||||
{
|
|
||||||
$request->validate([
|
|
||||||
'start_date' => 'required|date',
|
|
||||||
'end_date' => 'required|date|after_or_equal:start_date',
|
|
||||||
'initial_balance' => 'sometimes|numeric|min:0',
|
|
||||||
'exit' => 'sometimes|numeric|min:0',
|
|
||||||
'notes' => 'sometimes|string|max:1000',
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
// RESUMEN FINANCIERO
|
||||||
DB::beginTransaction();
|
fputcsv($file, ['RESUMEN FINANCIERO', '']);
|
||||||
|
fputcsv($file, ['Periodo Inicio', $cashCloses->last()?->opened_at]);
|
||||||
|
fputcsv($file, ['Periodo Fin', $cashCloses->first()?->closed_at]);
|
||||||
|
fputcsv($file, ['Total Ventas', number_format($totalIncome, 2)]);
|
||||||
|
fputcsv($file, ['Efectivo', number_format($totalCash, 2)]);
|
||||||
|
fputcsv($file, ['Tarjeta', number_format($totalCard, 2)]);
|
||||||
|
fputcsv($file, ['Transferencia', number_format($totalTransfer, 2)]);
|
||||||
|
fputcsv($file, ['Egresos', number_format($totalExit, 2)]);
|
||||||
|
fputcsv($file, []);
|
||||||
|
|
||||||
// Buscar ventas sin corte en el rango especificado
|
// VENTAS DETALLADAS
|
||||||
$availableSales = Sale::whereNull('cash_close_id')
|
fputcsv($file, ['VENTAS DETALLADAS']);
|
||||||
->whereBetween('sale_date', [$request->start_date, $request->end_date])
|
fputcsv($file, ['Nombre Comprador', 'ID SIM', 'Número Asignado', 'Paquete', 'Costo', 'Medio de Pago']);
|
||||||
->get();
|
|
||||||
|
|
||||||
if ($availableSales->isEmpty()) {
|
foreach ($detailedSales as $item) {
|
||||||
return ApiResponse::NOT_FOUND->response([
|
fputcsv($file, [
|
||||||
'message' => 'No se encontraron ventas disponibles para crear el corte de caja.',
|
$item->sale->client->full_name,
|
||||||
|
"'" . $item->simCard->iccid . "'",
|
||||||
|
"'" . $item->simCard->msisdn . "'",
|
||||||
|
$item->package->name,
|
||||||
|
number_format($item->package->price, 2),
|
||||||
|
ucfirst($item->sale->payment_method)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar que ninguna venta ya esté en otro corte (doble verificación)
|
fclose($file);
|
||||||
$salesWithClose = Sale::whereBetween('sale_date', [$request->start_date, $request->end_date])
|
};
|
||||||
->whereNotNull('cash_close_id')
|
|
||||||
->with('cashClose:id,status,opened_at,closed_at')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
if ($salesWithClose->isNotEmpty()) {
|
return response()->stream($callback, 200, $headers);
|
||||||
return ApiResponse::BAD_REQUEST->response([
|
|
||||||
'message' => 'Algunas ventas en este rango ya están asignadas a otros cortes.',
|
|
||||||
'conflicting_sales' => $salesWithClose->map(fn($s) => [
|
|
||||||
'sale_id' => $s->id,
|
|
||||||
'sale_date' => $s->sale_date,
|
|
||||||
'cash_close_id' => $s->cash_close_id,
|
|
||||||
'cash_close_status' => $s->cashClose->status ?? null,
|
|
||||||
]),
|
|
||||||
], 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calcular totales
|
|
||||||
$totalAmount = $availableSales->sum('total_amount');
|
|
||||||
$paymentMethods = $availableSales->groupBy('payment_method');
|
|
||||||
|
|
||||||
$incomeCash = $paymentMethods->get('cash', collect())->sum('total_amount');
|
|
||||||
$incomeCard = $paymentMethods->get('card', collect())->sum('total_amount');
|
|
||||||
$incomeTransfer = $paymentMethods->get('transfer', collect())->sum('total_amount');
|
|
||||||
|
|
||||||
// Crear el corte de caja
|
|
||||||
$cashClose = CashClose::create([
|
|
||||||
'user_id' => auth()->id(),
|
|
||||||
'opened_at' => $request->start_date,
|
|
||||||
'closed_at' => $request->end_date,
|
|
||||||
'initial_balance' => $request->input('initial_balance', 0),
|
|
||||||
'income' => $totalAmount,
|
|
||||||
'exit' => $request->input('exit', 0),
|
|
||||||
'income_cash' => $incomeCash,
|
|
||||||
'income_card' => $incomeCard,
|
|
||||||
'income_transfer' => $incomeTransfer,
|
|
||||||
'status' => 'closed',
|
|
||||||
'type' => 'manual',
|
|
||||||
'notes' => $request->input('notes'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Asignar las ventas al corte creado
|
|
||||||
Sale::whereIn('id', $availableSales->pluck('id'))
|
|
||||||
->update(['cash_close_id' => $cashClose->id]);
|
|
||||||
|
|
||||||
DB::commit();
|
|
||||||
|
|
||||||
$balanceFinal = $cashClose->initial_balance + $totalAmount - $cashClose->exit;
|
|
||||||
|
|
||||||
return ApiResponse::CREATED->response([
|
|
||||||
'message' => 'Corte de caja creado exitosamente',
|
|
||||||
'cash_close' => $cashClose->fresh('user'),
|
|
||||||
'resumen' => [
|
|
||||||
'periodo' => [
|
|
||||||
'inicio' => $cashClose->opened_at,
|
|
||||||
'fin' => $cashClose->closed_at,
|
|
||||||
],
|
|
||||||
'totales' => [
|
|
||||||
'fondo_inicial' => $cashClose->initial_balance,
|
|
||||||
'total_ventas' => $totalAmount,
|
|
||||||
'cantidad_ventas' => $availableSales->count(),
|
|
||||||
'efectivo' => $incomeCash,
|
|
||||||
'tarjeta' => $incomeCard,
|
|
||||||
'transferencia' => $incomeTransfer,
|
|
||||||
'egresos' => $cashClose->exit,
|
|
||||||
'balance_final' => $balanceFinal,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
DB::rollBack();
|
|
||||||
|
|
||||||
return ApiResponse::INTERNAL_ERROR->response([
|
|
||||||
'message' => 'Error al crear el corte de caja',
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deshacer (eliminar) un corte de caja
|
|
||||||
* Las ventas se liberan automáticamente (cash_close_id = null)
|
|
||||||
*/
|
|
||||||
public function undoCashClose(CashClose $cashClose)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
DB::beginTransaction();
|
|
||||||
|
|
||||||
// Verificar que el corte existe
|
|
||||||
if (!$cashClose) {
|
|
||||||
return ApiResponse::NOT_FOUND->response([
|
|
||||||
'message' => 'Corte de caja no encontrado.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtener información antes de eliminar
|
|
||||||
$salesCount = $cashClose->sales()->count();
|
|
||||||
$cashCloseId = $cashClose->id;
|
|
||||||
$cashCloseType = $cashClose->type;
|
|
||||||
|
|
||||||
// Liberar las ventas (setear cash_close_id a null)
|
|
||||||
Sale::where('cash_close_id', $cashClose->id)
|
|
||||||
->update(['cash_close_id' => null]);
|
|
||||||
|
|
||||||
// Eliminar el corte (soft delete)
|
|
||||||
$cashClose->delete();
|
|
||||||
|
|
||||||
DB::commit();
|
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
|
||||||
'message' => 'Corte de caja eliminado exitosamente',
|
|
||||||
'info' => [
|
|
||||||
'cash_close_id' => $cashCloseId,
|
|
||||||
'type' => $cashCloseType,
|
|
||||||
'sales_liberated' => $salesCount,
|
|
||||||
'nota' => 'Las ventas han sido liberadas y están disponibles para ser asignadas a otro corte.',
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
DB::rollBack();
|
|
||||||
|
|
||||||
return ApiResponse::INTERNAL_ERROR->response([
|
|
||||||
'message' => 'Error al eliminar el corte de caja',
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,7 +69,6 @@ public function expiring(Request $request)
|
|||||||
// Parámetros de consulta
|
// Parámetros de consulta
|
||||||
$customDays = $request->query('days');
|
$customDays = $request->query('days');
|
||||||
$period = $request->query('period');
|
$period = $request->query('period');
|
||||||
$packageId = $request->query('package_id'); // NUEVO
|
|
||||||
$withClientOnly = $request->boolean('with_client', false);
|
$withClientOnly = $request->boolean('with_client', false);
|
||||||
|
|
||||||
// Obtener paquetes activos con sus relaciones
|
// Obtener paquetes activos con sus relaciones
|
||||||
@ -84,11 +83,6 @@ public function expiring(Request $request)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrar por paquete específico si se especifica
|
|
||||||
if ($packageId !== null) {
|
|
||||||
$query->where('package_id', $packageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$activePackages = $query->get();
|
$activePackages = $query->get();
|
||||||
|
|
||||||
// Filtrar y calcular días restantes
|
// Filtrar y calcular días restantes
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
use App\Enums\SimCardStatus;
|
use App\Enums\SimCardStatus;
|
||||||
use App\Http\Requests\Netbien\SaleStoreRequest;
|
use App\Http\Requests\Netbien\SaleStoreRequest;
|
||||||
use App\Http\Requests\Netbien\SaleUpdateRequest;
|
use App\Http\Requests\Netbien\SaleUpdateRequest;
|
||||||
|
use App\Services\CashCloseService;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
@ -66,9 +67,11 @@ public function store(SaleStoreRequest $request)
|
|||||||
$total += $package->price;
|
$total += $package->price;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cashClose = CashCloseService::getOrCreateOpenCashClose();
|
||||||
|
|
||||||
$sale = Sale::create([
|
$sale = Sale::create([
|
||||||
'client_id' => $client->id,
|
'client_id' => $client->id,
|
||||||
'cash_close_id' => null,
|
'cash_close_id' => $cashClose->id,
|
||||||
'total_amount' => $total,
|
'total_amount' => $total,
|
||||||
'payment_method' => $request->payment_method,
|
'payment_method' => $request->payment_method,
|
||||||
'sale_date' => now(),
|
'sale_date' => now(),
|
||||||
|
|||||||
@ -13,13 +13,10 @@
|
|||||||
use App\Models\Client;
|
use App\Models\Client;
|
||||||
use App\Models\ClientSim;
|
use App\Models\ClientSim;
|
||||||
use App\Models\Packages;
|
use App\Models\Packages;
|
||||||
use App\Models\Sale;
|
|
||||||
use App\Models\SaleItem;
|
|
||||||
use App\Enums\SimCardStatus;
|
use App\Enums\SimCardStatus;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,14 +105,11 @@ public function destroy(SimCard $simCard)
|
|||||||
/* ---------------------Importar Excel--------------------------- */
|
/* ---------------------Importar Excel--------------------------- */
|
||||||
|
|
||||||
private $packageCache = [];
|
private $packageCache = [];
|
||||||
private $columnMap = [];
|
|
||||||
private $stats = [
|
private $stats = [
|
||||||
'created' => 0,
|
'created' => 0,
|
||||||
'updated' => 0,
|
|
||||||
'assigned' => 0,
|
'assigned' => 0,
|
||||||
'packages_created' => 0,
|
'packages_created' => 0,
|
||||||
'clients_created' => 0,
|
'clients_created' => 0,
|
||||||
'sales_created' => 0,
|
|
||||||
'errors' => [],
|
'errors' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -132,43 +126,15 @@ public function import(Request $request)
|
|||||||
$sheet = $spreadsheet->getActiveSheet();
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
$rows = $sheet->toArray();
|
$rows = $sheet->toArray();
|
||||||
|
|
||||||
// Leer encabezados de la primera fila
|
|
||||||
$this->buildColumnMap($rows[0]);
|
|
||||||
|
|
||||||
foreach ($rows as $index => $row) {
|
foreach ($rows as $index => $row) {
|
||||||
if ($index === 0) continue; // Saltar encabezados
|
if ($index === 0) continue;
|
||||||
|
|
||||||
// Iniciar transacción por fila
|
$this->processRow([
|
||||||
DB::beginTransaction();
|
'iccid' => $row[4] ?? null,
|
||||||
|
'msisdn' => $row[5] ?? null,
|
||||||
try {
|
'estado_de_la_sim' => $row[9] ?? null,
|
||||||
$this->processRow([
|
'usuario' => $row[8] ?? null,
|
||||||
'iccid' => $this->getColumnValue($row, 'ICCID'),
|
]);
|
||||||
'msisdn' => $this->getColumnValue($row, 'MSISDN'),
|
|
||||||
'paquetes' => $this->getColumnValue($row, 'PAQUETES'),
|
|
||||||
'usuario' => $this->getColumnValue($row, 'USUARIO'),
|
|
||||||
'fecha_venta' => $this->getColumnValue($row, 'FECHA_VENTA'),
|
|
||||||
'metodo_pago' => $this->getColumnValue($row, 'METODO_PAGO'),
|
|
||||||
], $index + 1);
|
|
||||||
|
|
||||||
// Commit después de cada fila exitosa
|
|
||||||
DB::commit();
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// Revertir solo esta fila
|
|
||||||
DB::rollBack();
|
|
||||||
|
|
||||||
// Agregar error pero continuar con las demás filas
|
|
||||||
$this->stats['errors'][] = [
|
|
||||||
'fila' => $index + 1,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
'datos' => [
|
|
||||||
'iccid' => $this->getColumnValue($row, 'ICCID'),
|
|
||||||
'msisdn' => $this->getColumnValue($row, 'MSISDN'),
|
|
||||||
'paquetes' => $this->getColumnValue($row, 'PAQUETES'),
|
|
||||||
'usuario' => $this->getColumnValue($row, 'USUARIO'),
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
return ApiResponse::OK->response([
|
||||||
@ -185,232 +151,49 @@ public function import(Request $request)
|
|||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Error en la importación',
|
'message' => 'Error en la importación',
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function processRow(array $row, int $rowNumber = 0)
|
private function processRow(array $row)
|
||||||
{
|
{
|
||||||
// Validar campos requeridos
|
// Validar campos requeridos
|
||||||
if (empty($row['iccid']) || empty($row['msisdn'])) {
|
if (empty($row['iccid']) || empty($row['msisdn'])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar o crear la SIM
|
|
||||||
$sim = SimCard::where('iccid', $row['iccid'])->first();
|
|
||||||
|
|
||||||
if (!$sim) {
|
|
||||||
// No existe, crearla
|
|
||||||
$sim = SimCard::create([
|
|
||||||
'iccid' => $row['iccid'],
|
|
||||||
'msisdn' => $row['msisdn'],
|
|
||||||
'status' => SimCardStatus::AVAILABLE,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->stats['created']++;
|
|
||||||
} else {
|
|
||||||
// Ya existe, actualizar (msisdn en caso de cambio)
|
|
||||||
$sim->update([
|
|
||||||
'msisdn' => $row['msisdn'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!isset($this->stats['updated'])) {
|
|
||||||
$this->stats['updated'] = 0;
|
|
||||||
}
|
|
||||||
$this->stats['updated']++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determinar si es una venta (tiene usuario Y paquete)
|
|
||||||
$hasUsuario = !empty($row['usuario']) &&
|
|
||||||
strtolower(trim($row['usuario'])) !== 'si' &&
|
|
||||||
strtolower(trim($row['usuario'])) !== 'no';
|
|
||||||
$hasPaquete = !empty($row['paquetes']);
|
|
||||||
|
|
||||||
if ($hasUsuario && $hasPaquete) {
|
|
||||||
// Es una venta - procesar como venta completa
|
|
||||||
$this->processSale($sim, $row);
|
|
||||||
} else {
|
|
||||||
// No es venta - solo asignar paquete y/o cliente si existen
|
|
||||||
if ($hasPaquete) {
|
|
||||||
$this->processPackageFromText($sim, $row);
|
|
||||||
}
|
|
||||||
if ($hasUsuario) {
|
|
||||||
$this->assignToClient($sim, $row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function processSale(SimCard $sim, array $row)
|
|
||||||
{
|
|
||||||
// Parsear el paquete desde el texto
|
|
||||||
$estadoSim = trim($row['paquetes'] ?? '');
|
|
||||||
$packageInfo = $this->parsePackageText($estadoSim);
|
|
||||||
|
|
||||||
if (!$packageInfo) {
|
|
||||||
$this->stats['errors'][] = [
|
|
||||||
'iccid' => $sim->iccid,
|
|
||||||
'estado_sim' => $estadoSim,
|
|
||||||
'reason' => 'No se pudo parsear el paquete para la venta'
|
|
||||||
];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtener o crear el paquete
|
|
||||||
$package = $this->getOrCreatePackage(
|
|
||||||
$packageInfo['type'],
|
|
||||||
$packageInfo['price']
|
|
||||||
);
|
|
||||||
|
|
||||||
//Evitar duplicados
|
|
||||||
$existingSaleItem = SaleItem::where('sim_card_id', $sim->id)
|
|
||||||
->where('package_id', $package->id)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if ($existingSaleItem) {
|
|
||||||
// Ya existe una venta para esta SIM+Paquete, salir
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buscar o crear el cliente
|
|
||||||
$usuario = trim($row['usuario'] ?? '');
|
|
||||||
$client = Client::where('full_name', $usuario)->first()
|
|
||||||
?? Client::where('full_name', 'LIKE', "%{$usuario}%")->first()
|
|
||||||
?? Client::where(function ($query) use ($usuario) {
|
|
||||||
$query->whereRaw("CONCAT(name, ' ', IFNULL(paternal,''), ' ', IFNULL(maternal,'')) LIKE ?", ["%{$usuario}%"]);
|
|
||||||
})->first();
|
|
||||||
|
|
||||||
if (!$client) {
|
|
||||||
$nameParts = $this->splitFullName($usuario);
|
|
||||||
|
|
||||||
$client = Client::create([
|
|
||||||
'full_name' => $usuario,
|
|
||||||
'name' => $nameParts['name'],
|
|
||||||
'paternal' => $nameParts['paternal'],
|
|
||||||
'maternal' => $nameParts['maternal'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->stats['clients_created']++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parsear fecha de venta
|
|
||||||
$saleDate = $this->parseSaleDate($row['fecha_venta'] ?? null);
|
|
||||||
|
|
||||||
// Validar y normalizar método de pago
|
|
||||||
$paymentMethod = $this->normalizePaymentMethod($row['metodo_pago'] ?? null);
|
|
||||||
|
|
||||||
// Crear la venta
|
|
||||||
$sale = Sale::create([
|
|
||||||
'client_id' => $client->id,
|
|
||||||
'cash_close_id' => null,
|
|
||||||
'total_amount' => $package->price,
|
|
||||||
'payment_method' => $paymentMethod,
|
|
||||||
'sale_date' => $saleDate,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Crear el item de venta
|
|
||||||
SaleItem::create([
|
|
||||||
'sale_id' => $sale->id,
|
|
||||||
'sim_card_id' => $sim->id,
|
|
||||||
'package_id' => $package->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Asignar SIM al cliente
|
|
||||||
$existingRelation = ClientSim::where('client_id', $client->id)
|
|
||||||
->where('sim_card_id', $sim->id)
|
|
||||||
->where('is_active', true)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
if (!$existingRelation) {
|
|
||||||
ClientSim::create([
|
|
||||||
'client_id' => $client->id,
|
|
||||||
'sim_card_id' => $sim->id,
|
|
||||||
'assigned_at' => $saleDate,
|
|
||||||
'is_active' => true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asignar paquete a la SIM
|
|
||||||
$hasActivePackage = $sim->packages()
|
|
||||||
->wherePivot('package_id', $package->id)
|
|
||||||
->wherePivot('is_active', true)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
if (!$hasActivePackage) {
|
|
||||||
$sim->packages()->attach($package->id, [
|
|
||||||
'activated_at' => $saleDate,
|
|
||||||
'is_active' => true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualizar status de la SIM
|
|
||||||
$sim->update(['status' => SimCardStatus::ASSIGNED]);
|
|
||||||
|
|
||||||
// stats
|
|
||||||
$this->stats['sales_created']++;
|
|
||||||
$this->stats['assigned']++;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseSaleDate($dateValue)
|
|
||||||
{
|
|
||||||
if (empty($dateValue)) {
|
|
||||||
return now()->startOfDay()->format('Y-m-d H:i:s');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si es numérico
|
|
||||||
if (is_numeric($dateValue)) {
|
|
||||||
try {
|
|
||||||
$excelBaseDate = new \DateTime('1899-12-30');
|
|
||||||
$excelBaseDate->modify("+{$dateValue} days");
|
|
||||||
return $excelBaseDate->format('Y-m-d') . ' 00:00:00';
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return now()->startOfDay()->format('Y-m-d H:i:s');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si es string, intentar parsear como DD/MM/YYYY
|
|
||||||
try {
|
try {
|
||||||
// Primero intentar formato DD/MM/YYYY explícitamente
|
DB::transaction(function () use ($row) {
|
||||||
$date = \Carbon\Carbon::createFromFormat('d/m/Y', $dateValue);
|
// Buscar o crear la SIM
|
||||||
return $date->startOfDay()->format('Y-m-d H:i:s');
|
$sim = SimCard::where('iccid', $row['iccid'])->first();
|
||||||
|
|
||||||
|
if (!$sim) {
|
||||||
|
// No existe, crearla
|
||||||
|
$sim = SimCard::create([
|
||||||
|
'iccid' => $row['iccid'],
|
||||||
|
'msisdn' => $row['msisdn'],
|
||||||
|
'status' => SimCardStatus::AVAILABLE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->stats['created']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->processPackageFromText($sim, $row);
|
||||||
|
// Asignar cliente
|
||||||
|
$this->assignToClient($sim, $row);
|
||||||
|
});
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Fallback: intentar parsear automáticamente
|
$this->stats['errors'][] = [
|
||||||
try {
|
'iccid' => $row['iccid'] ?? 'N/A',
|
||||||
return \Carbon\Carbon::parse($dateValue)->startOfDay()->format('Y-m-d H:i:s');
|
'error' => $e->getMessage(),
|
||||||
} catch (\Exception $e) {
|
];
|
||||||
return now()->startOfDay()->format('Y-m-d H:i:s');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizePaymentMethod($method)
|
|
||||||
{
|
|
||||||
if (empty($method)) {
|
|
||||||
return 'import';
|
|
||||||
}
|
|
||||||
|
|
||||||
$method = strtolower(trim($method));
|
|
||||||
|
|
||||||
// Mapear variaciones comunes
|
|
||||||
$methodMap = [
|
|
||||||
'cash' => 'cash',
|
|
||||||
'efectivo' => 'cash',
|
|
||||||
'cash' => 'cash',
|
|
||||||
'card' => 'card',
|
|
||||||
'tarjeta' => 'card',
|
|
||||||
'credito' => 'card',
|
|
||||||
'debito' => 'card',
|
|
||||||
'transfer' => 'transfer',
|
|
||||||
'transferencia' => 'transfer',
|
|
||||||
'import' => 'import',
|
|
||||||
'importacion' => 'import',
|
|
||||||
];
|
|
||||||
|
|
||||||
return $methodMap[$method] ?? 'import';
|
|
||||||
}
|
|
||||||
|
|
||||||
private function processPackageFromText(SimCard $sim, array $row)
|
private function processPackageFromText(SimCard $sim, array $row)
|
||||||
{
|
{
|
||||||
$estadoSim = trim($row['paquetes'] ?? '');
|
$estadoSim = trim($row['estado_de_la_sim'] ?? '');
|
||||||
|
|
||||||
if (empty($estadoSim)) {
|
if (empty($estadoSim)) {
|
||||||
return;
|
return;
|
||||||
@ -432,17 +215,10 @@ private function processPackageFromText(SimCard $sim, array $row)
|
|||||||
$packageInfo['price']
|
$packageInfo['price']
|
||||||
);
|
);
|
||||||
|
|
||||||
$hasActivePackage = $sim->packages()
|
$sim->packages()->attach($package->id, [
|
||||||
->wherePivot('package_id', $package->id)
|
'activated_at' => now(),
|
||||||
->wherePivot('is_active', true)
|
'is_active' => true,
|
||||||
->exists();
|
]);
|
||||||
|
|
||||||
if (!$hasActivePackage) {
|
|
||||||
$sim->packages()->attach($package->id, [
|
|
||||||
'activated_at' => now(),
|
|
||||||
'is_active' => true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function parsePackageText(string $text): ?array
|
private function parsePackageText(string $text): ?array
|
||||||
@ -475,14 +251,8 @@ private function getOrCreatePackage(string $type, float $price): Packages
|
|||||||
}
|
}
|
||||||
|
|
||||||
$package = Packages::firstOrCreate(
|
$package = Packages::firstOrCreate(
|
||||||
[
|
['name' => $type, 'price' => $price],
|
||||||
'name' => $type,
|
['period' => 0, 'data_limit' => 0]
|
||||||
'price' => (float) $price
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'period' => 0,
|
|
||||||
'data_limit' => 0,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($package->wasRecentlyCreated) {
|
if ($package->wasRecentlyCreated) {
|
||||||
@ -511,14 +281,22 @@ private function assignToClient(SimCard $sim, array $row)
|
|||||||
if (!$client) {
|
if (!$client) {
|
||||||
$nameParts = $this->splitFullName($usuario);
|
$nameParts = $this->splitFullName($usuario);
|
||||||
|
|
||||||
$client = Client::create([
|
try {
|
||||||
'full_name' => $usuario,
|
$client = Client::create([
|
||||||
'name' => $nameParts['name'],
|
'full_name' => $usuario,
|
||||||
'paternal' => $nameParts['paternal'],
|
'name' => $nameParts['name'],
|
||||||
'maternal' => $nameParts['maternal'],
|
'paternal' => $nameParts['paternal'],
|
||||||
]);
|
'maternal' => $nameParts['maternal'],
|
||||||
|
]);
|
||||||
|
|
||||||
$this->stats['clients_created']++;
|
$this->stats['clients_created']++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->stats['errors'][] = [
|
||||||
|
'usuario' => $usuario,
|
||||||
|
'error' => 'Error al crear cliente: ' . $e->getMessage()
|
||||||
|
];
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$existingRelation = ClientSim::where('client_id', $client->id)
|
$existingRelation = ClientSim::where('client_id', $client->id)
|
||||||
@ -530,16 +308,24 @@ private function assignToClient(SimCard $sim, array $row)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientSim::create([
|
try {
|
||||||
'client_id' => $client->id,
|
ClientSim::create([
|
||||||
'sim_card_id' => $sim->id,
|
'client_id' => $client->id,
|
||||||
'assigned_at' => now(),
|
'sim_card_id' => $sim->id,
|
||||||
'is_active' => true,
|
'assigned_at' => now(),
|
||||||
]);
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
$sim->update(['status' => SimCardStatus::ASSIGNED]);
|
$sim->update(['status' => SimCardStatus::ASSIGNED]);
|
||||||
|
|
||||||
$this->stats['assigned']++;
|
$this->stats['assigned']++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->stats['errors'][] = [
|
||||||
|
'iccid' => $sim->iccid,
|
||||||
|
'usuario' => $usuario,
|
||||||
|
'error' => 'Error al asignar cliente: ' . $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function splitFullName(string $fullName): array
|
private function splitFullName(string $fullName): array
|
||||||
@ -553,38 +339,4 @@ private function splitFullName(string $fullName): array
|
|||||||
'maternal' => $parts[2] ?? '',
|
'maternal' => $parts[2] ?? '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Construye un mapa de nombres de columnas a índices
|
|
||||||
*/
|
|
||||||
private function buildColumnMap(array $headers)
|
|
||||||
{
|
|
||||||
$this->columnMap = [];
|
|
||||||
|
|
||||||
foreach ($headers as $index => $header) {
|
|
||||||
// Normalizar el nombre de la columna (mayúsculas, sin espacios extra)
|
|
||||||
$normalizedHeader = strtoupper(trim($header ?? ''));
|
|
||||||
|
|
||||||
if (!empty($normalizedHeader)) {
|
|
||||||
$this->columnMap[$normalizedHeader] = $index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene el valor de una columna por su nombre
|
|
||||||
*/
|
|
||||||
private function getColumnValue(array $row, string $columnName)
|
|
||||||
{
|
|
||||||
$normalizedName = strtoupper(trim($columnName));
|
|
||||||
|
|
||||||
// Buscar en el mapa de columnas
|
|
||||||
if (isset($this->columnMap[$normalizedName])) {
|
|
||||||
$index = $this->columnMap[$normalizedName];
|
|
||||||
return $row[$index] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si no se encuentra la columna, retornar null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ public function rules(): array
|
|||||||
'maternal' => ['required', 'string'],
|
'maternal' => ['required', 'string'],
|
||||||
'email' => ['nullable', 'email'],
|
'email' => ['nullable', 'email'],
|
||||||
'phone' => ['nullable', 'string', 'max:10'],
|
'phone' => ['nullable', 'string', 'max:10'],
|
||||||
'rfc' => ['nullable', 'string', 'max:13'],
|
'rfc' => ['required', 'string', 'max:13'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ public function rules(): array
|
|||||||
'maternal' => ['sometimes', 'string', 'max:100'],
|
'maternal' => ['sometimes', 'string', 'max:100'],
|
||||||
'email' => ['sometimes', 'email'],
|
'email' => ['sometimes', 'email'],
|
||||||
'phone' => ['nullable', 'string', 'max:20'],
|
'phone' => ['nullable', 'string', 'max:20'],
|
||||||
'rfc' => ['nullable', 'string', 'max:13'],
|
'rfc' => ['sometimes', 'string', 'max:13'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,9 +26,9 @@ public function rules(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => ['required', 'string', 'max:80'],
|
'name' => ['required', 'string', 'max:80'],
|
||||||
'price' => ['required', 'numeric'],
|
'price' => ['required', 'integer'],
|
||||||
'period' => ['required', 'numeric'],
|
'period' => ['required', 'integer'],
|
||||||
'data_limit' => ['required', 'numeric'],
|
'data_limit' => ['required', 'integer'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,7 @@ public function rules(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'iccid' => ['required', 'string', 'max:25', 'unique:sim_cards,iccid'],
|
'iccid' => ['required', 'string', 'max:25', 'unique:sim_cards,iccid'],
|
||||||
'msisdn' => ['required', 'string', 'max:15', 'unique:sim_cards,msisdn'],
|
'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'],
|
||||||
'package_id' => ['nullable', 'integer', 'exists:packages,id'],
|
'package_id' => ['nullable', 'integer', 'exists:packages,id'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@ public function messages() : array
|
|||||||
|
|
||||||
'msisdn.required' => 'El campo MSISDN es obligatorio.',
|
'msisdn.required' => 'El campo MSISDN es obligatorio.',
|
||||||
'msisdn.string' => 'El campo MSISDN debe ser una cadena de texto.',
|
'msisdn.string' => 'El campo MSISDN debe ser una cadena de texto.',
|
||||||
'msisdn.max' => 'El campo MSISDN no debe exceder los 15 caracteres.',
|
'msisdn.max' => 'El campo MSISDN no debe exceder los 10 caracteres.',
|
||||||
'msisdn.unique' => 'El MSISDN ya está en uso.',
|
'msisdn.unique' => 'El MSISDN ya está en uso.',
|
||||||
|
|
||||||
'package_id.integer' => 'El paquete debe ser un número entero.',
|
'package_id.integer' => 'El paquete debe ser un número entero.',
|
||||||
|
|||||||
@ -27,9 +27,7 @@ public function authorize(): bool
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'iccid' => ['sometimes', 'string', 'max:20'],
|
'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'],
|
||||||
'msisdn' => ['required', 'string', 'max:11'],
|
|
||||||
'status' => ['sometimes', 'string', 'in:available,assigned'],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
<?php namespace App\Models;
|
<?php namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
||||||
|
|
||||||
class CashClose extends Model
|
class CashClose extends Model
|
||||||
{
|
{
|
||||||
use SoftDeletes;
|
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
'opened_at',
|
'opened_at',
|
||||||
@ -18,8 +15,6 @@ class CashClose extends Model
|
|||||||
'income_card',
|
'income_card',
|
||||||
'income_transfer',
|
'income_transfer',
|
||||||
'status',
|
'status',
|
||||||
'type',
|
|
||||||
'notes',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
@ -31,7 +26,6 @@ class CashClose extends Model
|
|||||||
'income_transfer' => 'decimal:2',
|
'income_transfer' => 'decimal:2',
|
||||||
'opened_at' => 'datetime',
|
'opened_at' => 'datetime',
|
||||||
'closed_at' => 'datetime',
|
'closed_at' => 'datetime',
|
||||||
'deleted_at' => 'datetime',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
33
app/Services/CashCloseService.php
Normal file
33
app/Services/CashCloseService.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\CashClose;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class CashCloseService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Obtiene el corte de caja abierto del día o crea uno nuevo
|
||||||
|
*/
|
||||||
|
public static function getOrCreateOpenCashClose()
|
||||||
|
{
|
||||||
|
$cashClose = CashClose::open()->first();
|
||||||
|
|
||||||
|
if (!$cashClose) {
|
||||||
|
$cashClose = CashClose::create([
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'opened_at' => now(),
|
||||||
|
'initial_balance' => 0,
|
||||||
|
'income' => 0,
|
||||||
|
'exit' => 0,
|
||||||
|
'income_cash' => 0,
|
||||||
|
'income_card' => 0,
|
||||||
|
'income_transfer' => 0,
|
||||||
|
'status' => 'open',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cashClose;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('cash_closes', function (Blueprint $table) {
|
|
||||||
$table->softDeletes()->after('updated_at');
|
|
||||||
$table->enum('type', ['daily', 'manual', 'import', 'adjustment'])->default('daily')->after('status');
|
|
||||||
$table->text('notes')->nullable()->after('type');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('cash_closes', function (Blueprint $table) {
|
|
||||||
$table->dropSoftDeletes();
|
|
||||||
$table->dropColumn(['type', 'notes']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -36,12 +36,9 @@
|
|||||||
Route::resource('sales', SaleController::class);
|
Route::resource('sales', SaleController::class);
|
||||||
|
|
||||||
Route::get('cash-closes', [CashCloseController::class, 'index']);
|
Route::get('cash-closes', [CashCloseController::class, 'index']);
|
||||||
|
Route::put('cash-closes/close', [CashCloseController::class, 'closeCashClose']);
|
||||||
Route::get('cash-closes/report', [CashCloseController::class, 'report']);
|
Route::get('cash-closes/report', [CashCloseController::class, 'report']);
|
||||||
Route::get('cash-closes/export', [CashCloseController::class, 'exportReport']);
|
Route::get('cash-closes/export', [CashCloseController::class, 'exportReport']);
|
||||||
|
|
||||||
Route::get('cash-closes/preview-range', [CashCloseController::class, 'previewSalesRange']);
|
|
||||||
Route::post('cash-closes/create-from-range', [CashCloseController::class, 'createFromRange']);
|
|
||||||
Route::delete('cash-closes/{cashClose}/undo', [CashCloseController::class, 'undoCashClose']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Rutas públicas */
|
/** Rutas públicas */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user