diff --git a/app/Http/Requests/App/InventoryStoreRequest.php b/app/Http/Requests/App/InventoryStoreRequest.php index ac93521..63fd6d1 100644 --- a/app/Http/Requests/App/InventoryStoreRequest.php +++ b/app/Http/Requests/App/InventoryStoreRequest.php @@ -25,6 +25,7 @@ public function rules(): array 'barcode' => ['nullable', 'string', 'unique:inventories,barcode'], 'category_id' => ['required', 'exists:categories,id'], 'stock' => ['nullable', 'integer', 'min:0'], + 'track_serials' => ['nullable', 'boolean'], // Campos de Price 'cost' => ['required', 'numeric', 'min:0'], diff --git a/app/Http/Requests/App/InventoryUpdateRequest.php b/app/Http/Requests/App/InventoryUpdateRequest.php index edf78cb..87d8e1a 100644 --- a/app/Http/Requests/App/InventoryUpdateRequest.php +++ b/app/Http/Requests/App/InventoryUpdateRequest.php @@ -27,6 +27,7 @@ public function rules(): array 'barcode' => ['nullable', 'string', 'unique:inventories,barcode,' . $inventoryId], 'category_id' => ['nullable', 'exists:categories,id'], 'stock' => ['nullable', 'integer', 'min:0'], + 'track_serials' => ['nullable', 'boolean'], // Campos de Price 'cost' => ['nullable', 'numeric', 'min:0'], diff --git a/app/Models/Inventory.php b/app/Models/Inventory.php index 697b911..1b9455f 100644 --- a/app/Models/Inventory.php +++ b/app/Models/Inventory.php @@ -21,11 +21,13 @@ class Inventory extends Model 'sku', 'barcode', 'stock', + 'track_serials', 'is_active', ]; protected $casts = [ 'is_active' => 'boolean', + 'track_serials' => 'boolean', ]; protected $appends = ['has_serials']; @@ -67,7 +69,9 @@ public function getAvailableStockAttribute(): int */ public function syncStock(): void { - $this->update(['stock' => $this->getAvailableStockAttribute()]); + if($this->track_serials) { + $this->update(['stock' => $this->getAvailableStockAttribute()]); + } } public function getHasSerialsAttribute(): bool diff --git a/app/Services/ProductService.php b/app/Services/ProductService.php index 88e0a3b..908c67b 100644 --- a/app/Services/ProductService.php +++ b/app/Services/ProductService.php @@ -15,6 +15,7 @@ public function createProduct(array $data) 'barcode' => $data['barcode'] ?? null, 'category_id' => $data['category_id'], 'stock' => $data['stock'] ?? 0, + 'track_serials' => $data['track_serials'] ?? false, ]); $price = Price::create([ @@ -38,6 +39,7 @@ public function updateProduct(Inventory $inventory, array $data) 'barcode' => $data['barcode'] ?? null, 'category_id' => $data['category_id'] ?? null, 'stock' => $data['stock'] ?? null, + 'track_serials' => $data['track_serials'] ?? null, ], fn($value) => $value !== null); if (!empty($inventoryData)) { diff --git a/app/Services/ReturnService.php b/app/Services/ReturnService.php index db1909f..56a5fb0 100644 --- a/app/Services/ReturnService.php +++ b/app/Services/ReturnService.php @@ -49,7 +49,7 @@ public function createReturn(array $data): Returns if ($item['quantity_returned'] > $maxReturnable) { throw new \Exception( "Solo puedes devolver {$maxReturnable} unidades de {$saleDetail->product_name}. " . - "Ya se devolvieron {$alreadyReturned} de {$saleDetail->quantity} vendidas." + "Ya se devolvieron {$alreadyReturned} de {$saleDetail->quantity} vendidas." ); } @@ -93,48 +93,65 @@ public function createReturn(array $data): Returns 'subtotal' => $saleDetail->unit_price * $item['quantity_returned'], ]); - // Gestionar seriales - if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) { - // Seriales específicos proporcionados - foreach ($item['serial_numbers'] as $serialNumber) { - $serial = InventorySerial::where('serial_number', $serialNumber) - ->where('sale_detail_id', $saleDetail->id) - ->where('status', 'vendido') - ->first(); - if (!$serial) { + $inventory = $saleDetail->inventory; + + if ($inventory->track_serials) { + // Validación de cantidad de seriales + if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) { + if (count($item['serial_numbers']) != $item['quantity_returned']) { throw new \Exception( - "El serial {$serialNumber} no pertenece a esta venta o ya fue devuelto." + "La cantidad de seriales proporcionados no coincide con la cantidad a devolver " . + "para {$saleDetail->product_name}." + ); + } + } + + // Gestionar seriales + if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) { + // Seriales específicos proporcionados + foreach ($item['serial_numbers'] as $serialNumber) { + $serial = InventorySerial::where('serial_number', $serialNumber) + ->where('sale_detail_id', $saleDetail->id) + ->where('status', 'vendido') + ->first(); + + if (!$serial) { + throw new \Exception( + "El serial {$serialNumber} no pertenece a esta venta o ya fue devuelto." + ); + } + + // Marcar como devuelto y vincular a devolución + $serial->markAsReturned($returnDetail->id); + + // Luego restaurar a disponible + $serial->restoreFromReturn(); + } + } else { + // Seleccionar automáticamente los primeros N seriales vendidos + $serials = InventorySerial::where('sale_detail_id', $saleDetail->id) + ->where('status', 'vendido') + ->limit($item['quantity_returned']) + ->get(); + + if ($serials->count() < $item['quantity_returned']) { + throw new \Exception( + "No hay suficientes seriales disponibles para devolver de {$saleDetail->product_name}" ); } - // Marcar como devuelto y vincular a devolución - $serial->markAsReturned($returnDetail->id); - - // Luego restaurar a disponible - $serial->restoreFromReturn(); + foreach ($serials as $serial) { + $serial->markAsReturned($returnDetail->id); + $serial->restoreFromReturn(); + } } + + // Sincronizar el stock del inventario + $saleDetail->inventory->syncStock(); } else { - // Seleccionar automáticamente los primeros N seriales vendidos - $serials = InventorySerial::where('sale_detail_id', $saleDetail->id) - ->where('status', 'vendido') - ->limit($item['quantity_returned']) - ->get(); - - if ($serials->count() < $item['quantity_returned']) { - throw new \Exception( - "No hay suficientes seriales disponibles para devolver de {$saleDetail->product_name}" - ); - } - - foreach ($serials as $serial) { - $serial->markAsReturned($returnDetail->id); - $serial->restoreFromReturn(); - } + $inventory->increment('stock', $item['quantity_returned']); } - - // Sincronizar el stock del inventario - $saleDetail->inventory->syncStock(); } // 5. Retornar con relaciones cargadas @@ -155,6 +172,7 @@ public function cancelReturn(Returns $return): Returns return DB::transaction(function () use ($return) { // Restaurar seriales a estado vendido foreach ($return->details as $detail) { + if(!$detail->inventory->track_serials) { $serials = InventorySerial::where('return_detail_id', $detail->id)->get(); foreach ($serials as $serial) { @@ -164,9 +182,12 @@ public function cancelReturn(Returns $return): Returns 'return_detail_id' => null, ]); } - // Sincronizar stock $detail->inventory->syncStock(); + } else { + // Restaurar stock numérico + $detail->inventory->increment('stock', $detail->quantity_returned); + } } // Eliminar la devolución (soft delete) @@ -194,15 +215,17 @@ public function getReturnableItems(Sale $sale): array $maxReturnable = $detail->quantity - $alreadyReturned; if ($maxReturnable > 0) { - // Obtener seriales vendidos que aún no han sido devueltos - $availableSerials = InventorySerial::where('sale_detail_id', $detail->id) - ->where('status', 'vendido') - ->get() - ->map(fn ($serial) => [ - 'serial_number' => $serial->serial_number, - 'status' => $serial->status, - ]); - + $availableSerials = []; + if ($detail->inventory->track_serials) { + // Obtener seriales vendidos que aún no han sido devueltos + $availableSerials = InventorySerial::where('sale_detail_id', $detail->id) + ->where('status', 'vendido') + ->get() + ->map(fn($serial) => [ + 'serial_number' => $serial->serial_number, + 'status' => $serial->status, + ]); + } $returnableItems[] = [ 'sale_detail_id' => $detail->id, 'inventory_id' => $detail->inventory_id, diff --git a/app/Services/SaleService.php b/app/Services/SaleService.php index a14d7c0..66402e8 100644 --- a/app/Services/SaleService.php +++ b/app/Services/SaleService.php @@ -55,7 +55,7 @@ public function createSale(array $data) // Obtener el inventario $inventory = Inventory::find($item['inventory_id']); - if ($inventory) { + 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) { @@ -88,6 +88,12 @@ public function createSale(array $data) // 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']); } } @@ -115,14 +121,17 @@ public function cancelSale(Sale $sale) // Restaurar seriales a disponible foreach ($sale->details as $detail) { - $serials = InventorySerial::where('sale_detail_id', $detail->id)->get(); - - foreach ($serials as $serial) { - $serial->markAsAvailable(); + 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); } - - // Sincronizar stock - $detail->inventory->syncStock(); } // Marcar venta como cancelada diff --git a/database/migrations/2026_01_26_143429_add_track_serials_to_inventory_table.php b/database/migrations/2026_01_26_143429_add_track_serials_to_inventory_table.php new file mode 100644 index 0000000..86259ce --- /dev/null +++ b/database/migrations/2026_01_26_143429_add_track_serials_to_inventory_table.php @@ -0,0 +1,28 @@ +boolean('track_serials')->default(false)->after('stock'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('inventories', function (Blueprint $table) { + $table->dropColumn('track_serials'); + }); + } +};