ADD: Corte de caja creado

This commit is contained in:
Juan Felipe Zapata Moreno 2025-11-05 16:24:22 -06:00
parent abfa2fe1fd
commit db49b127db
18 changed files with 486 additions and 21 deletions

View File

@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers\Netbien;
use App\Http\Controllers\Controller;
use App\Models\CashClose;
use App\Models\Sale;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Notsoweb\ApiResponse\Enums\ApiResponse;
/**
*
*/
class CashCloseController extends Controller
{
public function index(Request $request)
{
$query = CashClose::with('user:id,name')->withCount('sales');
if ($request->has('status')) {
$query->where('status', $request->status);
}
if ($request->has('date')) {
$query->whereDate('close_date', $request->date);
}
$cashCloses = $query->orderBy('id', 'asc')
->paginate(config('app.pagination'));
return ApiResponse::OK->response([
'cash_closes' => $cashCloses,
]);
}
public function CloseCashClose($id)
{
$today = now()->format('Y-m-d');
$cashClose = CashClose::findOrFail($id);
$totalSales = Sale::where('cash_close_id', $id)->sum('total_amount');
$paymentMethods = Sale::where('cash_close_id', $id)
->select('payment_method', DB::raw('SUM(total_amount) as total'))
->groupBy('payment_method')
->get();
$cashClose->update([
'income' => $totalSales,
'balance' => $totalSales - $cashClose->exit,
'payment_methods' => $paymentMethods,
'status' => 'closed',
'close_date' => $today,
]);
return ApiResponse::OK->response([
'message' => 'Corte de caja cerrado exitosamente.',
'data' => $cashClose,
]);
}
public function report($id)
{
// Información del corte de caja
$cashClose = CashClose::with('user:id,name')
->withCount('sales')
->findOrFail($id);
// Estadísticas por paquete (Total de Paquetes Vendidos por Tipo)
$packageStats = DB::table('sale_items')
->join('sales', 'sale_items.sale_id', '=', 'sales.id')
->join('packages', 'sale_items.package_id', '=', 'packages.id')
->where('sales.cash_close_id', $id)
->select(
'packages.name as paquete',
DB::raw('COUNT(*) as total_vendidos')
)
->groupBy('packages.id', 'packages.name')
->get();
// Estadísticas por duración (Total de Ventas por Duración)
$durationStats = DB::table('sale_items')
->join('sales', 'sale_items.sale_id', '=', 'sales.id')
->join('packages', 'sale_items.package_id', '=', 'packages.id')
->where('sales.cash_close_id', $id)
->select(
'packages.period as duracion_dias',
DB::raw('COUNT(DISTINCT sales.id) as total_ventas')
)
->groupBy('packages.period')
->orderBy('packages.period', 'asc')
->get();
// Reporte detallado de ventas
$detailedSales = DB::table('sales')
->join('clients', 'sales.client_id', '=', 'clients.id')
->join('sale_items', 'sales.id', '=', 'sale_items.sale_id')
->join('packages', 'sale_items.package_id', '=', 'packages.id')
->join('sim_cards', 'sale_items.sim_card_id', '=', 'sim_cards.id')
->where('sales.cash_close_id', $id)
->select(
DB::raw("CONCAT(clients.name, ' ', clients.paternal, ' ', clients.maternal) as nombre_comprador"),
'sim_cards.iccid as id_sim',
'sim_cards.msisdn as numero_asignado',
'packages.name as paquete',
'packages.price as costo',
'sales.payment_method as medio_pago'
)
->orderBy('sales.id', 'desc')
->get();
return ApiResponse::OK->response([
'cash_close' => $cashClose,
'ventas por paquete' => $packageStats,
'ventas por duracion' => $durationStats,
'ventas detalladas' => $detailedSales,
]);
}
}

View File

@ -11,6 +11,7 @@
use App\Enums\SimCardStatus;
use App\Http\Requests\Netbien\SaleStoreRequest;
use App\Http\Requests\Netbien\SaleUpdateRequest;
use App\Services\CashCloseService;
use Illuminate\Support\Facades\DB;
use Notsoweb\ApiResponse\Enums\ApiResponse;
@ -66,8 +67,11 @@ public function store(SaleStoreRequest $request)
$total += $package->price;
}
$cashClose = CashCloseService::getOrCreateOpenCashClose();
$sale = Sale::create([
'client_id' => $client->id,
'cash_close_id' => $cashClose->id,
'total_amount' => $total,
'payment_method' => $request->payment_method,
'sale_date' => now(),

View File

@ -32,6 +32,7 @@ public function rules(): array
'maternal' => ['required', 'string'],
'email' => ['nullable', 'email'],
'phone' => ['nullable', 'string', 'max:10'],
'rfc' => ['required', 'string', 'max:13'],
];
}

View File

@ -32,6 +32,7 @@ public function rules(): array
'maternal' => ['sometimes', 'string', 'max:100'],
'email' => ['sometimes', 'email'],
'phone' => ['nullable', 'string', 'max:20'],
'rfc' => ['sometimes', 'string', 'max:13'],
];
}

85
app/Models/CashClose.php Normal file
View File

@ -0,0 +1,85 @@
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
*
*/
class CashClose extends Model
{
protected $fillable = [
'balance',
'income',
'exit',
'expected_balance',
'difference',
'close_date',
'notes',
'status',
'user_id',
];
protected $casts = [
'balance' => 'decimal:2',
'income' => 'decimal:2',
'exit' => 'decimal:2',
'expected_balance' => 'decimal:2',
'difference' => 'decimal:2',
'close_date' => 'date',
];
/**
* Relación con el usuario que realizó el corte
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Relación con las ventas asociadas a este corte
*/
public function sales()
{
return $this->hasMany(Sale::class);
}
/**
* Scope para cortes abiertos
*/
public function scopeOpen($query)
{
return $query->where('status', 'open');
}
/**
* Scope para cortes cerrados
*/
public function scopeClosed($query)
{
return $query->where('status', 'closed');
}
/**
* Scope para cortes de una fecha específica
*/
public function scopeByDate($query, $date)
{
return $query->whereDate('close_date', $date);
}
/**
* Calcula automáticamente el balance
*/
public function calculateBalance()
{
$this->balance = $this->income - $this->exit;
if ($this->expected_balance) {
$this->difference = $this->balance - $this->expected_balance;
}
return $this->balance;
}
}

View File

@ -21,6 +21,7 @@ class Client extends Model
'maternal',
'email',
'phone',
'rfc',
];
public function sales()

View File

@ -17,6 +17,7 @@ class Sale extends Model
{
protected $fillable = [
'client_id',
'cash_close_id',
'total_amount',
'payment_method',
'sale_date',
@ -31,4 +32,9 @@ public function saleItems()
{
return $this->hasMany(SaleItem::class);
}
public function cashClose()
{
return $this->belongsTo(CashClose::class);
}
}

View File

@ -0,0 +1,31 @@
<?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()
{
$today = now()->format('Y-m-d');
$cashClose = CashClose::open()->byDate($today)->first();
if (!$cashClose) {
$cashClose = CashClose::create([
'close_date' => $today,
'income' => 0,
'exit' => 0,
'balance' => 0,
'status' => 'open',
'user_id' => Auth::id(),
]);
}
return $cashClose;
}
}

View File

@ -0,0 +1,33 @@
<?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::create('cash_closes', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null');
$table->decimal('balance', 10, 2);
$table->decimal('income', 10, 2);
$table->decimal('exit', 10, 2)->default(0);
$table->enum('status', ['open', 'closed', 'reviewed'])->default('open')->comment('Estado del corte');
$table->timestamp('close_date');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cash_closes');
}
};

View File

@ -0,0 +1,29 @@
<?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('sales', function (Blueprint $table) {
$table->foreignId('cash_close_id')->nullable()->after('client_id')->constrained('cash_closes')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('sales', function (Blueprint $table) {
$table->dropForeign(['cash_close_id']);
$table->dropColumn('cash_close_id');
});
}
};

View File

@ -0,0 +1,28 @@
<?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('clients', function (Blueprint $table) {
$table->string('rfc')->nullable()->after('phone');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('clients', function (Blueprint $table) {
$table->dropColumn('rfc');
});
}
};

View File

@ -0,0 +1,58 @@
<?php namespace Database\Seeders;
use App\Models\Client;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ClientSeeder extends Seeder
{
/**
* Ejecutar sembrado de base de datos
*/
public function run(): void
{
$clients = [
[
'name' => 'Juan',
'paternal' => 'Pérez',
'maternal' => 'Gómez',
'email' => 'juan.perez@example.com',
'phone' => '551234567890',
'rfc' => 'JUAP890123XXX',
],
[
'name' => 'María',
'paternal' => 'López',
'maternal' => 'Hernández',
'email' => 'maria.lopez@example.com',
'phone' => '551234567891',
'rfc' => 'MALO910203XXX',
],
[
'name' => 'María',
'paternal' => 'Hernández',
'maternal' => 'Cruz',
'email' => 'maria.hernandez@example.com',
'phone' => '555-1003',
],
[
'name' => 'Carlos',
'paternal' => 'Sánchez',
'maternal' => 'Ruiz',
'email' => 'carlos.sanchez@example.com',
'phone' => '555-1004',
],
[
'name' => 'Laura',
'paternal' => 'Gómez',
'maternal' => 'Flores',
'email' => 'laura.gomez@example.com',
'phone' => '555-1005',
]
];
foreach ($clients as $client) {
Client::create($client);
}
}
}

View File

@ -0,0 +1,25 @@
<?php namespace Database\Seeders;
/**
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
/**
* Descripción
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class PackageSeeder extends Seeder
{
/**
* Ejecutar sembrado de base de datos
*/
public function run(): void
{
//
}
}

View File

@ -1,25 +1,42 @@
<?php namespace Database\Seeders;
/**
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use App\Models\Packages;
/**
* Descripción
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
* @version 1.0.0
*/
class Packages extends Seeder
class PackagesSeeder extends Seeder
{
/**
* Ejecutar sembrado de base de datos
*/
public function run(): void
{
//
$packages = [
[
'name' => 'Paquete 1',
'price' => 100.00,
'period' => 15,
'data_limit' => 5,
],
[
'name' => 'Paquete 2',
'price' => 150.00,
'period' => 20,
'data_limit' => 10,
],
[
'name' => 'Paquete 3',
'price' => 200.00,
'period' => 25,
'data_limit' => 15,
],
[
'name' => 'Paquete Premium',
'price' => 250.00,
'period' => 30,
'data_limit' => 20,
],
];
foreach ($packages as $package) {
Packages::create($package);
}
}
}

View File

@ -3,14 +3,15 @@
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
*/
use App\Models\SimCard;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
/**
* Descripción
*
*
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
*
*
* @version 1.0.0
*/
class SimCardSeeder extends Seeder
@ -20,6 +21,25 @@ class SimCardSeeder extends Seeder
*/
public function run(): void
{
//
$availables=[
['iccid'=>'8986002212345678901','msisdn'=>'551234567890','status'=>'available'],
['iccid'=>'8986002212345678902','msisdn'=>'551234567891','status'=>'available'],
['iccid'=>'8986002212345678903','msisdn'=>'551234567892','status'=>'available'],
['iccid'=>'8986002212345678904','msisdn'=>'551234567893','status'=>'available'],
['iccid'=>'8986002212345678905','msisdn'=>'551234567894','status'=>'available'],
['iccid'=>'8986002212345678906','msisdn'=>'551234567895','status'=>'available'],
['iccid'=>'8986002212345678907','msisdn'=>'551234567896','status'=>'available'],
['iccid'=>'8986002212345678908','msisdn'=>'551234567897','status'=>'available'],
['iccid'=>'8986002212345678909','msisdn'=>'551234567898','status'=>'available'],
['iccid'=>'8986002212345678910','msisdn'=>'551234567899','status'=>'available'],
];
foreach ($availables as $simcard) {
SimCard::create([
'iccid' => $simcard['iccid'],
'msisdn' => $simcard['msisdn'],
'status' => $simcard['status'],
]);
}
}
}

View File

@ -5,7 +5,7 @@ services:
dockerfile: dockerfile
working_dir: /var/www/netbien
environment:
- DB_HOST=mysql
- DB_HOST=${DB_HOST}
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- DB_DATABASE=${DB_DATABASE}

View File

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Netbien\CashCloseController;
use App\Http\Controllers\Netbien\PackagesController;
use App\Http\Controllers\Netbien\SimCardController;
use App\Http\Controllers\Netbien\ClientController;
@ -30,6 +31,10 @@
Route::resource('clients', ClientController::class);
Route::resource('sales', SaleController::class);
Route::get('cash-closes', [CashCloseController::class, 'index']);
Route::put('cash-closes/{id}/close', [CashCloseController::class, 'CloseCashClose']);
Route::get('cash-closes/{id}/report', [CashCloseController::class, 'report']);
});
/** Rutas públicas */