wip: catalog de almacenes, entrada, salida y traspaso
This commit is contained in:
parent
656492251b
commit
3cac336e10
37
app/Http/Controllers/App/InventoryMovementController.php
Normal file
37
app/Http/Controllers/App/InventoryMovementController.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php namespace App\Http\Controllers\App;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class InventoryMovementController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function inventory()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
18
app/Http/Controllers/App/WarehouseController.php
Normal file
18
app/Http/Controllers/App/WarehouseController.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php namespace App\Http\Controllers;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class WarehouseController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@ -32,6 +32,23 @@ class Inventory extends Model
|
||||
|
||||
protected $appends = ['has_serials', 'inventory_value'];
|
||||
|
||||
public function warehouses() {
|
||||
return $this->belongsToMany(Warehouse::class, 'inventory_warehouse')
|
||||
->withPivot('stock', 'min_stock', 'max_stock')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
// Obtener stock total en todos los almacenes
|
||||
public function getTotalStockAttribute(): int {
|
||||
return $this->warehouses()->sum('inventory_warehouse.stock');
|
||||
}
|
||||
|
||||
// Sincronizar stock global
|
||||
public function syncGlobalStock(): void {
|
||||
$this->update(['stock' => $this->total_stock]);
|
||||
}
|
||||
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
@ -84,6 +101,6 @@ public function getHasSerialsAttribute(): bool
|
||||
*/
|
||||
public function getInventoryValueAttribute(): float
|
||||
{
|
||||
return $this->stock * ($this->price?->cost ?? 0);
|
||||
return $this->total_stock * ($this->price?->cost ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Models/InventoryMovement.php
Normal file
64
app/Models/InventoryMovement.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InventoryMovement extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'inventory_id',
|
||||
'warehouse_from_id',
|
||||
'warehouse_to_id',
|
||||
'movement_type',
|
||||
'quantity',
|
||||
'reference_type',
|
||||
'reference_id',
|
||||
'user_id',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
// Relaciones
|
||||
public function inventory() {
|
||||
return $this->belongsTo(Inventory::class);
|
||||
}
|
||||
|
||||
public function warehouseFrom() {
|
||||
return $this->belongsTo(Warehouse::class, 'warehouse_from_id');
|
||||
}
|
||||
|
||||
public function warehouseTo() {
|
||||
return $this->belongsTo(Warehouse::class, 'warehouse_to_id');
|
||||
}
|
||||
|
||||
public function user() {
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
// Relación polimórfica para la referencia
|
||||
public function reference() {
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopeByType($query, string $type) {
|
||||
return $query->where('movement_type', $type);
|
||||
}
|
||||
|
||||
public function scopeEntry($query) {
|
||||
return $query->where('movement_type', 'entry');
|
||||
}
|
||||
|
||||
public function scopeExit($query) {
|
||||
return $query->where('movement_type', 'exit');
|
||||
}
|
||||
|
||||
public function scopeTransfer($query) {
|
||||
return $query->where('movement_type', 'transfer');
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ class InventorySerial extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'inventory_id',
|
||||
'warehouse_id',
|
||||
'serial_number',
|
||||
'status',
|
||||
'sale_detail_id',
|
||||
@ -28,6 +29,11 @@ public function inventory()
|
||||
return $this->belongsTo(Inventory::class);
|
||||
}
|
||||
|
||||
public function warehouse()
|
||||
{
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
|
||||
public function saleDetail()
|
||||
{
|
||||
return $this->belongsTo(SaleDetail::class);
|
||||
@ -49,11 +55,11 @@ public function isAvailable(): bool
|
||||
/**
|
||||
* Marcar como vendido
|
||||
*/
|
||||
public function markAsSold(int $saleDetailId): void
|
||||
{
|
||||
public function markAsSold(int $saleDetailId, ?int $warehouseId = null): void {
|
||||
$this->update([
|
||||
'status' => 'vendido',
|
||||
'sale_detail_id' => $saleDetailId,
|
||||
'warehouse_id' => $warehouseId,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
44
app/Models/InventoryWarehouse.php
Normal file
44
app/Models/InventoryWarehouse.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class InventoryWarehouse extends Model
|
||||
{
|
||||
protected $table = 'inventory_warehouse';
|
||||
|
||||
protected $fillable = [
|
||||
'inventory_id',
|
||||
'warehouse_id',
|
||||
'stock',
|
||||
'min_stock',
|
||||
'max_stock',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'stock' => 'integer',
|
||||
'min_stock' => 'integer',
|
||||
'max_stock' => 'integer',
|
||||
];
|
||||
|
||||
// Relaciones
|
||||
public function inventory() {
|
||||
return $this->belongsTo(Inventory::class);
|
||||
}
|
||||
|
||||
public function warehouse() {
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
|
||||
// Métodos útiles
|
||||
public function isLowStock(): bool {
|
||||
return $this->min_stock && $this->stock <= $this->min_stock;
|
||||
}
|
||||
|
||||
public function isOverStock(): bool {
|
||||
return $this->max_stock && $this->stock >= $this->max_stock;
|
||||
}
|
||||
|
||||
public function hasAvailableStock(int $quantity): bool {
|
||||
return $this->stock >= $quantity;
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ class SaleDetail extends Model
|
||||
protected $fillable = [
|
||||
'sale_id',
|
||||
'inventory_id',
|
||||
'warehouse_id',
|
||||
'product_name',
|
||||
'quantity',
|
||||
'unit_price',
|
||||
@ -33,6 +34,10 @@ class SaleDetail extends Model
|
||||
'discount_amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function warehouse() {
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
|
||||
public function sale()
|
||||
{
|
||||
return $this->belongsTo(Sale::class);
|
||||
|
||||
61
app/Models/Warehouse.php
Normal file
61
app/Models/Warehouse.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Modelo para almacenes
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class Warehouse extends Model
|
||||
{
|
||||
protected $fillable = ['code', 'name', 'is_active', 'is_main'];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'is_main' => 'boolean',
|
||||
];
|
||||
|
||||
// Relaciones
|
||||
public function inventories()
|
||||
{
|
||||
return $this->belongsToMany(Inventory::class, 'inventory_warehouse')
|
||||
->withPivot('stock', 'min_stock', 'max_stock')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function inventorySerials()
|
||||
{
|
||||
return $this->hasMany(InventorySerial::class);
|
||||
}
|
||||
|
||||
public function movementsFrom()
|
||||
{
|
||||
return $this->hasMany(InventoryMovement::class, 'warehouse_from_id');
|
||||
}
|
||||
|
||||
public function movementsTo()
|
||||
{
|
||||
return $this->hasMany(InventoryMovement::class, 'warehouse_to_id');
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeMain($query)
|
||||
{
|
||||
return $query->where('is_main', true);
|
||||
}
|
||||
}
|
||||
191
app/Services/InventoryMovementService.php
Normal file
191
app/Services/InventoryMovementService.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php namespace App\Services;
|
||||
|
||||
use App\Models\Inventory;
|
||||
use App\Models\InventoryMovement;
|
||||
use App\Models\InventoryWarehouse;
|
||||
use App\Models\Warehouse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Servicio para gestión de movimientos de inventario
|
||||
*/
|
||||
class InventoryMovementService
|
||||
{
|
||||
/**
|
||||
* Entrada de inventario (compra, ajuste positivo)
|
||||
*/
|
||||
public function entry(array $data): InventoryMovement
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$inventory = Inventory::findOrFail($data['inventory_id']);
|
||||
$warehouse = Warehouse::findOrFail($data['warehouse_id']);
|
||||
$quantity = $data['quantity'];
|
||||
|
||||
// Actualizar stock en inventory_warehouse
|
||||
$this->updateWarehouseStock($inventory->id, $warehouse->id, $quantity);
|
||||
|
||||
// Sincronizar stock global
|
||||
$inventory->syncGlobalStock();
|
||||
|
||||
// Registrar movimiento
|
||||
return InventoryMovement::create([
|
||||
'inventory_id' => $inventory->id,
|
||||
'warehouse_from_id' => null, // Entrada externa
|
||||
'warehouse_to_id' => $warehouse->id,
|
||||
'movement_type' => $data['movement_type'] ?? 'entry',
|
||||
'quantity' => $quantity,
|
||||
'user_id' => auth()->id(),
|
||||
'notes' => $data['notes'] ?? null,
|
||||
'reference_type' => $data['reference_type'] ?? null,
|
||||
'reference_id' => $data['reference_id'] ?? null,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Salida de inventario (merma, ajuste negativo, robo)
|
||||
*/
|
||||
public function exit(array $data): InventoryMovement
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$inventory = Inventory::findOrFail($data['inventory_id']);
|
||||
$warehouse = Warehouse::findOrFail($data['warehouse_id']);
|
||||
$quantity = $data['quantity'];
|
||||
|
||||
// Validar stock disponible
|
||||
$this->validateStock($inventory->id, $warehouse->id, $quantity);
|
||||
|
||||
// Decrementar stock en inventory_warehouse
|
||||
$this->updateWarehouseStock($inventory->id, $warehouse->id, -$quantity);
|
||||
|
||||
// Sincronizar stock global
|
||||
$inventory->syncGlobalStock();
|
||||
|
||||
// Registrar movimiento
|
||||
return InventoryMovement::create([
|
||||
'inventory_id' => $inventory->id,
|
||||
'warehouse_from_id' => $warehouse->id,
|
||||
'warehouse_to_id' => null, // Salida externa
|
||||
'movement_type' => $data['movement_type'] ?? 'exit',
|
||||
'quantity' => $quantity,
|
||||
'user_id' => auth()->id(),
|
||||
'notes' => $data['notes'] ?? null,
|
||||
'reference_type' => $data['reference_type'] ?? null,
|
||||
'reference_id' => $data['reference_id'] ?? null,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Traspaso entre almacenes
|
||||
*/
|
||||
public function transfer(array $data): InventoryMovement
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$inventory = Inventory::findOrFail($data['inventory_id']);
|
||||
$warehouseFrom = Warehouse::findOrFail($data['warehouse_from_id']);
|
||||
$warehouseTo = Warehouse::findOrFail($data['warehouse_to_id']);
|
||||
$quantity = $data['quantity'];
|
||||
|
||||
// Validar que no sea el mismo almacén
|
||||
if ($warehouseFrom->id === $warehouseTo->id) {
|
||||
throw new \Exception('No se puede traspasar al mismo almacén.');
|
||||
}
|
||||
|
||||
// Validar stock disponible en almacén origen
|
||||
$this->validateStock($inventory->id, $warehouseFrom->id, $quantity);
|
||||
|
||||
// Decrementar en origen
|
||||
$this->updateWarehouseStock($inventory->id, $warehouseFrom->id, -$quantity);
|
||||
|
||||
// Incrementar en destino
|
||||
$this->updateWarehouseStock($inventory->id, $warehouseTo->id, $quantity);
|
||||
|
||||
// Stock global no cambia, no es necesario sincronizar
|
||||
|
||||
// Registrar movimiento
|
||||
return InventoryMovement::create([
|
||||
'inventory_id' => $inventory->id,
|
||||
'warehouse_from_id' => $warehouseFrom->id,
|
||||
'warehouse_to_id' => $warehouseTo->id,
|
||||
'movement_type' => 'transfer',
|
||||
'quantity' => $quantity,
|
||||
'user_id' => auth()->id(),
|
||||
'notes' => $data['notes'] ?? null,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar stock en inventory_warehouse
|
||||
*/
|
||||
protected function updateWarehouseStock(int $inventoryId, int $warehouseId, int $quantityChange): void
|
||||
{
|
||||
$record = InventoryWarehouse::firstOrCreate(
|
||||
[
|
||||
'inventory_id' => $inventoryId,
|
||||
'warehouse_id' => $warehouseId,
|
||||
],
|
||||
['stock' => 0]
|
||||
);
|
||||
|
||||
$newStock = $record->stock + $quantityChange;
|
||||
|
||||
// No permitir stock negativo
|
||||
if ($newStock < 0) {
|
||||
throw new \Exception('Stock insuficiente en el almacén.');
|
||||
}
|
||||
|
||||
$record->update(['stock' => $newStock]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar stock disponible
|
||||
*/
|
||||
protected function validateStock(int $inventoryId, int $warehouseId, int $requiredQuantity): void
|
||||
{
|
||||
$record = InventoryWarehouse::where('inventory_id', $inventoryId)
|
||||
->where('warehouse_id', $warehouseId)
|
||||
->first();
|
||||
|
||||
$availableStock = $record?->stock ?? 0;
|
||||
|
||||
if ($availableStock < $requiredQuantity) {
|
||||
throw new \Exception("Stock insuficiente. Disponible: {$availableStock}, Requerido: {$requiredQuantity}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registrar movimiento de venta
|
||||
*/
|
||||
public function recordSale(int $inventoryId, int $warehouseId, int $quantity, int $saleId): InventoryMovement
|
||||
{
|
||||
return InventoryMovement::create([
|
||||
'inventory_id' => $inventoryId,
|
||||
'warehouse_from_id' => $warehouseId,
|
||||
'warehouse_to_id' => null,
|
||||
'movement_type' => 'sale',
|
||||
'quantity' => $quantity,
|
||||
'reference_type' => 'App\Models\Sale',
|
||||
'reference_id' => $saleId,
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registrar movimiento de devolución
|
||||
*/
|
||||
public function recordReturn(int $inventoryId, int $warehouseId, int $quantity, int $returnId): InventoryMovement
|
||||
{
|
||||
return InventoryMovement::create([
|
||||
'inventory_id' => $inventoryId,
|
||||
'warehouse_from_id' => null,
|
||||
'warehouse_to_id' => $warehouseId,
|
||||
'movement_type' => 'return',
|
||||
'quantity' => $quantity,
|
||||
'reference_type' => 'App\Models\Returns',
|
||||
'reference_id' => $returnId,
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
115
app/Services/WarehouseService.php
Normal file
115
app/Services/WarehouseService.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php namespace App\Services;
|
||||
|
||||
use App\Models\Warehouse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Servicio para gestión de almacenes
|
||||
*/
|
||||
class WarehouseService
|
||||
{
|
||||
/**
|
||||
* Obtener almacén principal
|
||||
*/
|
||||
public function getMainWarehouse(): ?Warehouse
|
||||
{
|
||||
return Warehouse::main()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crear nuevo almacén
|
||||
*/
|
||||
public function createWarehouse(array $data): Warehouse
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
// Si se marca como principal, desactivar otros principales
|
||||
if ($data['is_main'] ?? false) {
|
||||
Warehouse::where('is_main', true)->update(['is_main' => false]);
|
||||
}
|
||||
|
||||
// Si no hay almacenes principales, este debe serlo
|
||||
if (!Warehouse::main()->exists()) {
|
||||
$data['is_main'] = true;
|
||||
}
|
||||
|
||||
return Warehouse::create($data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar almacén
|
||||
*/
|
||||
public function updateWarehouse(Warehouse $warehouse, array $data): Warehouse
|
||||
{
|
||||
return DB::transaction(function () use ($warehouse, $data) {
|
||||
// Si se marca como principal, desactivar otros principales
|
||||
if (isset($data['is_main']) && $data['is_main']) {
|
||||
Warehouse::where('id', '!=', $warehouse->id)
|
||||
->where('is_main', true)
|
||||
->update(['is_main' => false]);
|
||||
}
|
||||
|
||||
// No permitir desactivar el último almacén principal
|
||||
if (isset($data['is_main']) && !$data['is_main'] && $warehouse->is_main) {
|
||||
if (Warehouse::main()->count() === 1) {
|
||||
throw new \Exception('No se puede desactivar el único almacén principal. Asigna otro almacén como principal primero.');
|
||||
}
|
||||
}
|
||||
|
||||
$warehouse->update($data);
|
||||
return $warehouse->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activar/Desactivar almacén
|
||||
*/
|
||||
public function toggleActive(Warehouse $warehouse): Warehouse
|
||||
{
|
||||
// No permitir desactivar el almacén principal
|
||||
if ($warehouse->is_main && $warehouse->is_active) {
|
||||
throw new \Exception('No se puede desactivar el almacén principal.');
|
||||
}
|
||||
|
||||
$warehouse->update(['is_active' => !$warehouse->is_active]);
|
||||
return $warehouse->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener stock de un almacén
|
||||
*/
|
||||
public function getWarehouseStock(Warehouse $warehouse)
|
||||
{
|
||||
return $warehouse->inventories()
|
||||
->with(['category', 'price'])
|
||||
->select('inventories.*')
|
||||
->get()
|
||||
->map(function ($inventory) use ($warehouse) {
|
||||
$pivot = $inventory->warehouses()
|
||||
->where('warehouse_id', $warehouse->id)
|
||||
->first();
|
||||
|
||||
return [
|
||||
'inventory_id' => $inventory->id,
|
||||
'name' => $inventory->name,
|
||||
'sku' => $inventory->sku,
|
||||
'category' => $inventory->category?->name,
|
||||
'stock' => $pivot?->pivot->stock ?? 0,
|
||||
'min_stock' => $pivot?->pivot->min_stock,
|
||||
'max_stock' => $pivot?->pivot->max_stock,
|
||||
'cost' => $inventory->price?->cost,
|
||||
'retail_price' => $inventory->price?->retail_price,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar que existe almacén principal
|
||||
*/
|
||||
public function ensureMainWarehouse(): void
|
||||
{
|
||||
if (!Warehouse::main()->exists()) {
|
||||
throw new \Exception('No existe un almacén principal configurado.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
<?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::create('warehouses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->string('name');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->boolean('is_main')->default(false);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('warehouses');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
<?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::create('inventory_warehouse', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('inventory_id')->constrained()->onDelete('restrict');
|
||||
$table->foreignId('warehouse_id')->constrained()->onDelete('restrict');
|
||||
$table->integer('stock')->default(0);
|
||||
$table->integer('min_stock')->nullable()->default(0);
|
||||
$table->integer('max_stock')->nullable()->default(0);
|
||||
$table->timestamps();
|
||||
$table->unique(['inventory_id', 'warehouse_id']);
|
||||
$table->index(['warehouse_id', 'stock']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('inventory_warehouse');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
<?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::create('inventory_movements', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('inventory_id')->constrained('inventories')->onDelete('restrict');
|
||||
$table->foreignId('warehouse_from_id')->nullable()->constrained('warehouses')->onDelete('restrict');
|
||||
$table->foreignId('warehouse_to_id')->nullable()->constrained('warehouses')->onDelete('restrict');
|
||||
$table->enum('movement_type', ['entry', 'exit', 'transfer', 'sale', 'return']);
|
||||
$table->integer('quantity');
|
||||
$table->string('reference_type')->nullable();
|
||||
$table->unsignedBigInteger('reference_id')->nullable();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('restrict');
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamp('created_at');
|
||||
|
||||
$table->index(['inventory_id', 'created_at']);
|
||||
$table->index('warehouse_from_id');
|
||||
$table->index('warehouse_to_id');
|
||||
$table->index(['reference_type', 'reference_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('inventory_movements');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?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('inventory_serials', function (Blueprint $table) {
|
||||
$table->foreignId('warehouse_id')->nullable()->after('inventory_id')->constrained('warehouses')->onDelete('restrict');
|
||||
$table->index(['warehouse_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('inventory_serials', function (Blueprint $table) {
|
||||
$table->dropForeign(['warehouse_id']);
|
||||
$table->dropIndex(['warehouse_id', 'status']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
<?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('sale_details', function (Blueprint $table) {
|
||||
$table->foreignId('warehouse_id')->nullable()->after('inventory_id')->constrained('warehouses')->onDelete('restrict');
|
||||
|
||||
$table->index('warehouse_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('sale_details', function (Blueprint $table) {
|
||||
$table->dropForeign(['warehouse_id']);
|
||||
$table->dropIndex(['warehouse_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// 1. Crear almacén principal
|
||||
$mainWarehouse = DB::table('warehouses')->insertGetId([
|
||||
'code' => 'ALM-PRINCIPAL',
|
||||
'name' => 'Almacén Principal',
|
||||
'is_active' => true,
|
||||
'is_main' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// 2. Migrar stock de inventories a inventory_warehouse
|
||||
$inventories = DB::table('inventories')->whereNull('deleted_at')->get();
|
||||
|
||||
foreach ($inventories as $inventory) {
|
||||
DB::table('inventory_warehouse')->insert([
|
||||
'inventory_id' => $inventory->id,
|
||||
'warehouse_id' => $mainWarehouse,
|
||||
'stock' => $inventory->stock,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. Asignar almacén principal a todos los seriales existentes
|
||||
DB::table('inventory_serials')->update([
|
||||
'warehouse_id' => $mainWarehouse,
|
||||
]);
|
||||
|
||||
// 4. Asignar almacén principal a sale_details existentes (nullable, opcional)
|
||||
DB::table('sale_details')
|
||||
->whereNull('warehouse_id')
|
||||
->update(['warehouse_id' => $mainWarehouse]);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
// Rollback: eliminar asignaciones de warehouse
|
||||
DB::table('inventory_serials')->update(['warehouse_id' => null]);
|
||||
DB::table('sale_details')->update(['warehouse_id' => null]);
|
||||
DB::table('inventory_warehouse')->truncate();
|
||||
DB::table('warehouses')->truncate();
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user