ADD: Corte de caja
This commit is contained in:
parent
569fbd09d7
commit
599bb68ce6
138
app/Http/Controllers/CashRegisterController.php
Normal file
138
app/Http/Controllers/CashRegisterController.php
Normal file
@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CashRegister;
|
||||
use App\Services\CashRegisterService;
|
||||
use Illuminate\Http\Request;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
class CashRegisterController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected CashRegisterService $cashRegisterService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Listar cortes de caja
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = CashRegister::with('user')->orderBy('opened_at', 'desc');
|
||||
|
||||
// Filtro por rango de fechas
|
||||
if ($request->has('from') && $request->has('to')) {
|
||||
$query->whereBetween('opened_at', [$request->from, $request->to]);
|
||||
}
|
||||
|
||||
// Filtro por estado
|
||||
if ($request->has('status')) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
$registers = $query->paginate(config('app.pagination'));
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'registers' => $registers
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ver caja actual del usuario
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
$register = CashRegister::where('user_id', auth()->id())
|
||||
->where('status', 'open')
|
||||
->with(['user', 'sales'])
|
||||
->first();
|
||||
|
||||
if (!$register) {
|
||||
return ApiResponse::OK->response([
|
||||
'register' => null,
|
||||
'message' => 'No tienes una caja abierta.'
|
||||
]);
|
||||
}
|
||||
|
||||
$summary = $this->cashRegisterService->getCurrentSummary($register);
|
||||
|
||||
return ApiResponse::OK->response($summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ver detalle de un corte específico
|
||||
*/
|
||||
public function show(CashRegister $register)
|
||||
{
|
||||
$register->load(['user', 'sales.details']);
|
||||
|
||||
$summary = [
|
||||
'register' => $register,
|
||||
'payment_summary' => $register->getTotalsByPaymentMethod(),
|
||||
];
|
||||
|
||||
return ApiResponse::OK->response($summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abrir caja
|
||||
*/
|
||||
public function open(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'initial_cash' => ['required', 'numeric', 'min:0'],
|
||||
], [
|
||||
'initial_cash.required' => 'El efectivo inicial es obligatorio.',
|
||||
'initial_cash.numeric' => 'El efectivo inicial debe ser un número.',
|
||||
'initial_cash.min' => 'El efectivo inicial no puede ser negativo.',
|
||||
]);
|
||||
|
||||
try {
|
||||
$register = $this->cashRegisterService->openRegister([
|
||||
'user_id' => auth()->id(),
|
||||
'initial_cash' => $request->initial_cash,
|
||||
]);
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'model' => $register,
|
||||
'message' => 'Caja abierta exitosamente.'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cerrar caja
|
||||
*/
|
||||
public function close(CashRegister $register, Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'final_cash' => ['required', 'numeric', 'min:0'],
|
||||
'notes' => ['nullable', 'string', 'max:500'],
|
||||
], [
|
||||
'final_cash.required' => 'El efectivo final es obligatorio.',
|
||||
'final_cash.numeric' => 'El efectivo final debe ser un número.',
|
||||
'final_cash.min' => 'El efectivo final no puede ser negativo.',
|
||||
]);
|
||||
|
||||
try {
|
||||
$closedRegister = $this->cashRegisterService->closeRegister($register, [
|
||||
'final_cash' => $request->final_cash,
|
||||
'notes' => $request->notes,
|
||||
]);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'model' => $closedRegister,
|
||||
'message' => 'Caja cerrada exitosamente.'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
app/Models/CashRegister.php
Normal file
62
app/Models/CashRegister.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CashRegister extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'opened_at',
|
||||
'closed_at',
|
||||
'initial_cash',
|
||||
'final_cash',
|
||||
'expected_cash',
|
||||
'difference',
|
||||
'total_sales',
|
||||
'sales_count',
|
||||
'notes',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'opened_at' => 'datetime',
|
||||
'closed_at' => 'datetime',
|
||||
'initial_cash' => 'decimal:2',
|
||||
'final_cash' => 'decimal:2',
|
||||
'expected_cash' => 'decimal:2',
|
||||
'difference' => 'decimal:2',
|
||||
'total_sales' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function sales()
|
||||
{
|
||||
return $this->hasMany(Sale::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar si la caja está abierta
|
||||
*/
|
||||
public function isOpen(): bool
|
||||
{
|
||||
return $this->status === 'open';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular totales por método de pago
|
||||
*/
|
||||
public function getTotalsByPaymentMethod()
|
||||
{
|
||||
return $this->sales()
|
||||
->where('status', 'completed')
|
||||
->selectRaw('payment_method, SUM(total) as total, COUNT(*) as count')
|
||||
->groupBy('payment_method')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ class Sale extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'cash_register_id',
|
||||
'invoice_number',
|
||||
'subtotal',
|
||||
'tax',
|
||||
@ -40,4 +41,9 @@ public function details()
|
||||
{
|
||||
return $this->hasMany(SaleDetail::class);
|
||||
}
|
||||
|
||||
public function cashRegister()
|
||||
{
|
||||
return $this->belongsTo(CashRegister::class);
|
||||
}
|
||||
}
|
||||
|
||||
95
app/Services/CashRegisterService.php
Normal file
95
app/Services/CashRegisterService.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\CashRegister;
|
||||
use App\Models\Sale;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CashRegisterService
|
||||
{
|
||||
/**
|
||||
* Abrir caja
|
||||
*/
|
||||
public function openRegister(array $data)
|
||||
{
|
||||
// Verificar que el usuario no tenga una caja abierta
|
||||
$openRegister = CashRegister::where('user_id', $data['user_id'])
|
||||
->where('status', 'open')
|
||||
->first();
|
||||
|
||||
if ($openRegister) {
|
||||
throw new \Exception('Ya tienes una caja abierta. Debes cerrarla antes de abrir una nueva.');
|
||||
}
|
||||
|
||||
return CashRegister::create([
|
||||
'user_id' => $data['user_id'],
|
||||
'opened_at' => now(),
|
||||
'initial_cash' => $data['initial_cash'] ?? 0,
|
||||
'status' => 'open',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cerrar caja
|
||||
*/
|
||||
public function closeRegister(CashRegister $register, array $data)
|
||||
{
|
||||
if ($register->status === 'closed') {
|
||||
throw new \Exception('Esta caja ya está cerrada.');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($register, $data) {
|
||||
// Calcular ventas en efectivo
|
||||
$cashSales = Sale::where('cash_register_id', $register->id)
|
||||
->where('payment_method', 'cash')
|
||||
->where('status', 'completed')
|
||||
->sum('total');
|
||||
|
||||
// Calcular efectivo esperado
|
||||
$expectedCash = $register->initial_cash + $cashSales;
|
||||
|
||||
// Calcular diferencia
|
||||
$finalCash = $data['final_cash'];
|
||||
$difference = $finalCash - $expectedCash;
|
||||
|
||||
// Actualizar caja
|
||||
$register->update([
|
||||
'closed_at' => now(),
|
||||
'final_cash' => $finalCash,
|
||||
'expected_cash' => $expectedCash,
|
||||
'difference' => $difference,
|
||||
'total_sales' => Sale::where('cash_register_id', $register->id)
|
||||
->where('status', 'completed')
|
||||
->sum('total'),
|
||||
'sales_count' => Sale::where('cash_register_id', $register->id)
|
||||
->where('status', 'completed')
|
||||
->count(),
|
||||
'notes' => $data['notes'] ?? null,
|
||||
'status' => 'closed',
|
||||
]);
|
||||
|
||||
return $register->fresh(['user', 'sales']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener resumen de caja actual
|
||||
*/
|
||||
public function getCurrentSummary(CashRegister $register)
|
||||
{
|
||||
$sales = Sale::where('cash_register_id', $register->id)
|
||||
->where('status', 'completed')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'register' => $register,
|
||||
'total_cash' => $sales->where('payment_method', 'cash')->sum('total'),
|
||||
'total_credit_card' => $sales->where('payment_method', 'credit_card')->sum('total'),
|
||||
'total_debit_card' => $sales->where('payment_method', 'debit_card')->sum('total'),
|
||||
'total_sales' => $sales->sum('total'),
|
||||
'sales_count' => $sales->count(),
|
||||
'expected_cash' => $register->initial_cash + $sales->where('payment_method', 'cash')->sum('total'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ public function createSale(array $data)
|
||||
// 1. Crear la venta principal
|
||||
$sale = Sale::create([
|
||||
'user_id' => $data['user_id'],
|
||||
'cash_register_id' => $data['cash_register_id'] ?? $this->getCurrentCashRegister($data['user_id']),
|
||||
'invoice_number' => $data['invoice_number'] ?? $this->generateInvoiceNumber(),
|
||||
'subtotal' => $data['subtotal'],
|
||||
'tax' => $data['tax'],
|
||||
@ -95,4 +96,13 @@ private function generateInvoiceNumber(): string
|
||||
|
||||
return $prefix . $date . '-' . str_pad($sequential, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function getCurrentCashRegister($userId)
|
||||
{
|
||||
$register = \App\Models\CashRegister::where('user_id', $userId)
|
||||
->where('status', 'open')
|
||||
->first();
|
||||
|
||||
return $register ? $register->id : null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cash_registers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('restrict');
|
||||
$table->timestamp('opened_at');
|
||||
$table->timestamp('closed_at')->nullable();
|
||||
$table->decimal('initial_cash', 10, 2)->default(0);
|
||||
$table->decimal('final_cash', 10, 2)->nullable();
|
||||
$table->decimal('expected_cash', 10, 2)->nullable();
|
||||
$table->decimal('difference', 10, 2)->nullable();
|
||||
$table->decimal('total_sales', 10, 2)->default(0);
|
||||
$table->integer('sales_count')->default(0);
|
||||
$table->text('notes')->nullable();
|
||||
$table->enum('status', ['open', 'closed'])->default('open');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'status']);
|
||||
$table->index('opened_at');
|
||||
});
|
||||
|
||||
Schema::table('sales', function (Blueprint $table) {
|
||||
$table->foreignId('cash_register_id')->nullable()->after('user_id')->constrained()->onDelete('restrict');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('sales', function (Blueprint $table) {
|
||||
$table->dropForeign(['cash_register_id']);
|
||||
$table->dropColumn('cash_register_id');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('cash_registers');
|
||||
}
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\App\CashRegisterController;
|
||||
use App\Http\Controllers\App\CategoryController;
|
||||
use App\Http\Controllers\App\InventoryController;
|
||||
use App\Http\Controllers\App\PriceController;
|
||||
@ -37,6 +38,12 @@
|
||||
Route::resource('/sales', SaleController::class);
|
||||
Route::put('/sales/{sale}/cancel', [SaleController::class, 'cancel']);
|
||||
|
||||
// Rutas de caja
|
||||
Route::get('/', [CashRegisterController::class, 'index']);
|
||||
Route::get('/current', [CashRegisterController::class, 'current']);
|
||||
Route::get('/{register}', [CashRegisterController::class, 'show']);
|
||||
Route::post('/open', [CashRegisterController::class, 'open']);
|
||||
Route::put('/{register}/close', [CashRegisterController::class, 'close']);
|
||||
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user