feat: corrección al apartado de inventario, productos que no tiene numero de serie

This commit is contained in:
Juan Felipe Zapata Moreno 2026-01-26 16:39:11 -06:00
parent b9e694f6ca
commit 91f27c4e4a
7 changed files with 122 additions and 54 deletions

View File

@ -25,6 +25,7 @@ public function rules(): array
'barcode' => ['nullable', 'string', 'unique:inventories,barcode'], 'barcode' => ['nullable', 'string', 'unique:inventories,barcode'],
'category_id' => ['required', 'exists:categories,id'], 'category_id' => ['required', 'exists:categories,id'],
'stock' => ['nullable', 'integer', 'min:0'], 'stock' => ['nullable', 'integer', 'min:0'],
'track_serials' => ['nullable', 'boolean'],
// Campos de Price // Campos de Price
'cost' => ['required', 'numeric', 'min:0'], 'cost' => ['required', 'numeric', 'min:0'],

View File

@ -27,6 +27,7 @@ public function rules(): array
'barcode' => ['nullable', 'string', 'unique:inventories,barcode,' . $inventoryId], 'barcode' => ['nullable', 'string', 'unique:inventories,barcode,' . $inventoryId],
'category_id' => ['nullable', 'exists:categories,id'], 'category_id' => ['nullable', 'exists:categories,id'],
'stock' => ['nullable', 'integer', 'min:0'], 'stock' => ['nullable', 'integer', 'min:0'],
'track_serials' => ['nullable', 'boolean'],
// Campos de Price // Campos de Price
'cost' => ['nullable', 'numeric', 'min:0'], 'cost' => ['nullable', 'numeric', 'min:0'],

View File

@ -21,11 +21,13 @@ class Inventory extends Model
'sku', 'sku',
'barcode', 'barcode',
'stock', 'stock',
'track_serials',
'is_active', 'is_active',
]; ];
protected $casts = [ protected $casts = [
'is_active' => 'boolean', 'is_active' => 'boolean',
'track_serials' => 'boolean',
]; ];
protected $appends = ['has_serials']; protected $appends = ['has_serials'];
@ -67,8 +69,10 @@ public function getAvailableStockAttribute(): int
*/ */
public function syncStock(): void public function syncStock(): void
{ {
if($this->track_serials) {
$this->update(['stock' => $this->getAvailableStockAttribute()]); $this->update(['stock' => $this->getAvailableStockAttribute()]);
} }
}
public function getHasSerialsAttribute(): bool public function getHasSerialsAttribute(): bool
{ {

View File

@ -15,6 +15,7 @@ public function createProduct(array $data)
'barcode' => $data['barcode'] ?? null, 'barcode' => $data['barcode'] ?? null,
'category_id' => $data['category_id'], 'category_id' => $data['category_id'],
'stock' => $data['stock'] ?? 0, 'stock' => $data['stock'] ?? 0,
'track_serials' => $data['track_serials'] ?? false,
]); ]);
$price = Price::create([ $price = Price::create([
@ -38,6 +39,7 @@ public function updateProduct(Inventory $inventory, array $data)
'barcode' => $data['barcode'] ?? null, 'barcode' => $data['barcode'] ?? null,
'category_id' => $data['category_id'] ?? null, 'category_id' => $data['category_id'] ?? null,
'stock' => $data['stock'] ?? null, 'stock' => $data['stock'] ?? null,
'track_serials' => $data['track_serials'] ?? null,
], fn($value) => $value !== null); ], fn($value) => $value !== null);
if (!empty($inventoryData)) { if (!empty($inventoryData)) {

View File

@ -93,6 +93,20 @@ public function createReturn(array $data): Returns
'subtotal' => $saleDetail->unit_price * $item['quantity_returned'], 'subtotal' => $saleDetail->unit_price * $item['quantity_returned'],
]); ]);
$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(
"La cantidad de seriales proporcionados no coincide con la cantidad a devolver " .
"para {$saleDetail->product_name}."
);
}
}
// Gestionar seriales // Gestionar seriales
if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) { if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) {
// Seriales específicos proporcionados // Seriales específicos proporcionados
@ -135,6 +149,9 @@ public function createReturn(array $data): Returns
// Sincronizar el stock del inventario // Sincronizar el stock del inventario
$saleDetail->inventory->syncStock(); $saleDetail->inventory->syncStock();
} else {
$inventory->increment('stock', $item['quantity_returned']);
}
} }
// 5. Retornar con relaciones cargadas // 5. Retornar con relaciones cargadas
@ -155,6 +172,7 @@ public function cancelReturn(Returns $return): Returns
return DB::transaction(function () use ($return) { return DB::transaction(function () use ($return) {
// Restaurar seriales a estado vendido // Restaurar seriales a estado vendido
foreach ($return->details as $detail) { foreach ($return->details as $detail) {
if(!$detail->inventory->track_serials) {
$serials = InventorySerial::where('return_detail_id', $detail->id)->get(); $serials = InventorySerial::where('return_detail_id', $detail->id)->get();
foreach ($serials as $serial) { foreach ($serials as $serial) {
@ -164,9 +182,12 @@ public function cancelReturn(Returns $return): Returns
'return_detail_id' => null, 'return_detail_id' => null,
]); ]);
} }
// Sincronizar stock // Sincronizar stock
$detail->inventory->syncStock(); $detail->inventory->syncStock();
} else {
// Restaurar stock numérico
$detail->inventory->increment('stock', $detail->quantity_returned);
}
} }
// Eliminar la devolución (soft delete) // Eliminar la devolución (soft delete)
@ -194,15 +215,17 @@ public function getReturnableItems(Sale $sale): array
$maxReturnable = $detail->quantity - $alreadyReturned; $maxReturnable = $detail->quantity - $alreadyReturned;
if ($maxReturnable > 0) { if ($maxReturnable > 0) {
$availableSerials = [];
if ($detail->inventory->track_serials) {
// Obtener seriales vendidos que aún no han sido devueltos // Obtener seriales vendidos que aún no han sido devueltos
$availableSerials = InventorySerial::where('sale_detail_id', $detail->id) $availableSerials = InventorySerial::where('sale_detail_id', $detail->id)
->where('status', 'vendido') ->where('status', 'vendido')
->get() ->get()
->map(fn ($serial) => [ ->map(fn($serial) => [
'serial_number' => $serial->serial_number, 'serial_number' => $serial->serial_number,
'status' => $serial->status, 'status' => $serial->status,
]); ]);
}
$returnableItems[] = [ $returnableItems[] = [
'sale_detail_id' => $detail->id, 'sale_detail_id' => $detail->id,
'inventory_id' => $detail->inventory_id, 'inventory_id' => $detail->inventory_id,

View File

@ -55,7 +55,7 @@ public function createSale(array $data)
// Obtener el inventario // Obtener el inventario
$inventory = Inventory::find($item['inventory_id']); $inventory = Inventory::find($item['inventory_id']);
if ($inventory) { if ($inventory->track_serials) {
// Si se proporcionaron números de serie específicos // Si se proporcionaron números de serie específicos
if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) { if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) {
foreach ($item['serial_numbers'] as $serialNumber) { foreach ($item['serial_numbers'] as $serialNumber) {
@ -88,6 +88,12 @@ public function createSale(array $data)
// Sincronizar el stock // Sincronizar el stock
$inventory->syncStock(); $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 // Restaurar seriales a disponible
foreach ($sale->details as $detail) { foreach ($sale->details as $detail) {
if ($detail->inventory->track_serials) {
// Restaurar seriales
$serials = InventorySerial::where('sale_detail_id', $detail->id)->get(); $serials = InventorySerial::where('sale_detail_id', $detail->id)->get();
foreach ($serials as $serial) { foreach ($serials as $serial) {
$serial->markAsAvailable(); $serial->markAsAvailable();
} }
// Sincronizar stock
$detail->inventory->syncStock(); $detail->inventory->syncStock();
} else {
// Restaurar stock numérico
$detail->inventory->increment('stock', $detail->quantity);
}
} }
// Marcar venta como cancelada // Marcar venta como cancelada

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('inventories', function (Blueprint $table) {
$table->boolean('track_serials')->default(false)->after('stock');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('inventories', function (Blueprint $table) {
$table->dropColumn('track_serials');
});
}
};