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,7 +69,9 @@ public function getAvailableStockAttribute(): int
*/ */
public function syncStock(): void public function syncStock(): void
{ {
$this->update(['stock' => $this->getAvailableStockAttribute()]); if($this->track_serials) {
$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

@ -49,7 +49,7 @@ public function createReturn(array $data): Returns
if ($item['quantity_returned'] > $maxReturnable) { if ($item['quantity_returned'] > $maxReturnable) {
throw new \Exception( throw new \Exception(
"Solo puedes devolver {$maxReturnable} unidades de {$saleDetail->product_name}. " . "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'], '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( 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 foreach ($serials as $serial) {
$serial->markAsReturned($returnDetail->id); $serial->markAsReturned($returnDetail->id);
$serial->restoreFromReturn();
// Luego restaurar a disponible }
$serial->restoreFromReturn();
} }
// Sincronizar el stock del inventario
$saleDetail->inventory->syncStock();
} else { } else {
// Seleccionar automáticamente los primeros N seriales vendidos $inventory->increment('stock', $item['quantity_returned']);
$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();
}
} }
// Sincronizar el stock del inventario
$saleDetail->inventory->syncStock();
} }
// 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) {
// Obtener seriales vendidos que aún no han sido devueltos $availableSerials = [];
$availableSerials = InventorySerial::where('sale_detail_id', $detail->id) if ($detail->inventory->track_serials) {
->where('status', 'vendido') // Obtener seriales vendidos que aún no han sido devueltos
->get() $availableSerials = InventorySerial::where('sale_detail_id', $detail->id)
->map(fn ($serial) => [ ->where('status', 'vendido')
'serial_number' => $serial->serial_number, ->get()
'status' => $serial->status, ->map(fn($serial) => [
]); 'serial_number' => $serial->serial_number,
'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) {
$serials = InventorySerial::where('sale_detail_id', $detail->id)->get(); if ($detail->inventory->track_serials) {
// Restaurar seriales
foreach ($serials as $serial) { $serials = InventorySerial::where('sale_detail_id', $detail->id)->get();
$serial->markAsAvailable(); 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 // 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');
});
}
};