172 lines
6.4 KiB
PHP
172 lines
6.4 KiB
PHP
<?php namespace App\Services;
|
|
|
|
use App\Models\CashRegister;
|
|
use App\Models\Sale;
|
|
use App\Models\SaleDetail;
|
|
use App\Models\Inventory;
|
|
use App\Models\InventorySerial;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class SaleService
|
|
{
|
|
/**
|
|
* Crear una nueva venta con sus detalles
|
|
*
|
|
*/
|
|
public function createSale(array $data)
|
|
{
|
|
return DB::transaction(function () use ($data) {
|
|
// Calcular el cambio si es pago en efectivo
|
|
$cashReceived = null;
|
|
$change = null;
|
|
|
|
if ($data['payment_method'] === 'cash' && isset($data['cash_received'])) {
|
|
$cashReceived = $data['cash_received'];
|
|
$change = $cashReceived - $data['total'];
|
|
}
|
|
|
|
// 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'],
|
|
'total' => $data['total'],
|
|
'cash_received' => $cashReceived,
|
|
'change' => $change,
|
|
'payment_method' => $data['payment_method'],
|
|
'status' => $data['status'] ?? 'completed',
|
|
]);
|
|
|
|
// 2. Crear los detalles de la venta y asignar seriales
|
|
foreach ($data['items'] as $item) {
|
|
// Crear detalle de venta
|
|
$saleDetail = SaleDetail::create([
|
|
'sale_id' => $sale->id,
|
|
'inventory_id' => $item['inventory_id'],
|
|
'product_name' => $item['product_name'],
|
|
'quantity' => $item['quantity'],
|
|
'unit_price' => $item['unit_price'],
|
|
'subtotal' => $item['subtotal'],
|
|
]);
|
|
|
|
// Obtener el inventario
|
|
$inventory = Inventory::find($item['inventory_id']);
|
|
|
|
if ($inventory->track_serials) {
|
|
// Si se proporcionaron números de serie específicos
|
|
if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) {
|
|
foreach ($item['serial_numbers'] as $serialNumber) {
|
|
$serial = InventorySerial::where('inventory_id', $inventory->id)
|
|
->where('serial_number', $serialNumber)
|
|
->where('status', 'disponible')
|
|
->first();
|
|
|
|
if ($serial) {
|
|
$serial->markAsSold($saleDetail->id);
|
|
} else {
|
|
throw new \Exception("Serial {$serialNumber} no disponible");
|
|
}
|
|
}
|
|
} else {
|
|
// Asignar automáticamente los primeros N seriales disponibles
|
|
$serials = InventorySerial::where('inventory_id', $inventory->id)
|
|
->where('status', 'disponible')
|
|
->limit($item['quantity'])
|
|
->get();
|
|
|
|
if ($serials->count() < $item['quantity']) {
|
|
throw new \Exception("Stock insuficiente de seriales para {$item['product_name']}");
|
|
}
|
|
|
|
foreach ($serials as $serial) {
|
|
$serial->markAsSold($saleDetail->id);
|
|
}
|
|
}
|
|
|
|
// Sincronizar el stock
|
|
$inventory->syncStock();
|
|
} else {
|
|
if ($inventory->stock < $item['quantity']) {
|
|
throw new \Exception("Stock insuficiente para {$item['product_name']}");
|
|
}
|
|
|
|
$inventory->decrement('stock', $item['quantity']);
|
|
}
|
|
}
|
|
|
|
// 3. Retornar la venta con sus relaciones cargadas
|
|
return $sale->load(['details.inventory', 'details.serials', 'user']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Cancelar una venta y restaurar el stock
|
|
*
|
|
*/
|
|
public function cancelSale(Sale $sale)
|
|
{
|
|
return DB::transaction(function () use ($sale) {
|
|
// Verificar que la venta esté completada
|
|
if ($sale->status !== 'completed') {
|
|
throw new \Exception('Solo se pueden cancelar ventas completadas.');
|
|
}
|
|
|
|
// Verificar que la venta no tenga devoluciones procesadas
|
|
if ($sale->returns()->exists()) {
|
|
throw new \Exception('No se puede cancelar una venta que tiene devoluciones procesadas.');
|
|
}
|
|
|
|
// Restaurar seriales a disponible
|
|
foreach ($sale->details as $detail) {
|
|
if ($detail->inventory->track_serials) {
|
|
// Restaurar seriales
|
|
$serials = InventorySerial::where('sale_detail_id', $detail->id)->get();
|
|
foreach ($serials as $serial) {
|
|
$serial->markAsAvailable();
|
|
}
|
|
$detail->inventory->syncStock();
|
|
} else {
|
|
// Restaurar stock numérico
|
|
$detail->inventory->increment('stock', $detail->quantity);
|
|
}
|
|
}
|
|
|
|
// Marcar venta como cancelada
|
|
$sale->update(['status' => 'cancelled']);
|
|
|
|
return $sale->fresh(['details.inventory', 'details.serials', 'user']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generar número de factura único
|
|
* Formato: INV-YYYYMMDD-0001
|
|
*/
|
|
private function generateInvoiceNumber(): string
|
|
{
|
|
$prefix = 'INV-';
|
|
$date = now()->format('Ymd');
|
|
|
|
// Obtener la última venta del día
|
|
$lastSale = Sale::whereDate('created_at', today())
|
|
->orderBy('id', 'desc')
|
|
->first();
|
|
|
|
// Incrementar secuencial
|
|
$sequential = $lastSale ? (intval(substr($lastSale->invoice_number, -4)) + 1) : 1;
|
|
|
|
return $prefix . $date . '-' . str_pad($sequential, 4, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
private function getCurrentCashRegister($userId)
|
|
{
|
|
$register = CashRegister::where('user_id', $userId)
|
|
->where('status', 'open')
|
|
->first();
|
|
|
|
return $register ? $register->id : null;
|
|
}
|
|
}
|