vernova/app/Http/Controllers/InventoryController.php
2026-07-26 19:58:27 +03:30

442 lines
15 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Item;
use App\Models\Warehouse;
use App\Models\InventoryTransaction;
use App\Models\Project;
use App\Models\Currency;
use App\Helpers\CurrencyHelper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class InventoryController extends Controller
{
// ==========================================
// DASHBOARD
// ==========================================
public function index()
{
$totalItems = Item::count();
$activeItems = Item::where('is_active', true)->count();
$totalWarehouses = Warehouse::where('is_active', true)->count();
$recentTransactions = InventoryTransaction::with(['item', 'warehouse', 'createdBy'])
->orderBy('created_at', 'desc')
->limit(10)
->get();
// Low stock items (below min_stock or zero)
$lowStockItems = Item::where('is_active', true)
->with(['inventoryTransactions'])
->get()
->filter(function ($item) {
$stock = $this->getCurrentStockForItem($item->id);
$minStock = $item->min_stock ?? 0;
return $stock <= $minStock;
})
->take(10);
return view('inventory.index', compact(
'totalItems', 'activeItems', 'totalWarehouses',
'recentTransactions', 'lowStockItems'
));
}
// ==========================================
// ITEMS CRUD
// ==========================================
public function items(Request $request)
{
$query = Item::with(['inventoryTransactions.currency', 'inventoryTransactions.warehouse']);
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('code', 'like', "%{$search}%")
->orWhere('category', 'like', "%{$search}%");
});
}
if ($request->filled('category')) {
$query->where('category', $request->category);
}
if ($request->filled('is_active')) {
$query->where('is_active', $request->boolean('is_active'));
}
$items = $query->orderBy('name')->paginate(20)->appends($request->query());
$categories = Item::distinct()->pluck('category')->filter()->values();
// Calculate current stock for each item
$items->through(function ($item) {
$item->current_stock = $this->getCurrentStockForItem($item->id);
return $item;
});
return view('inventory.items', compact('items', 'categories'));
}
public function createItem()
{
$this->authorize('create', Item::class);
$item = null;
$categories = Item::distinct()->pluck('category')->filter()->values();
return view('inventory.item-form', compact('item', 'categories'));
}
public function storeItem(Request $request)
{
$this->authorize('create', Item::class);
$validated = $request->validate([
'name' => 'required|string|max:255',
'code' => 'nullable|string|max:50|unique:items,code',
'unit' => 'required|string|max:50',
'category' => 'nullable|string|max:100',
'description' => 'nullable|string',
'min_stock' => 'nullable|numeric|min:0',
'is_active' => 'boolean',
]);
$validated['tenant_id'] = $this->getTenantId();
$validated['is_active'] = $request->boolean('is_active', true);
$item = Item::create($validated);
return redirect()
->route('inventory.items')
->with('success', __('inventory.item_created'));
}
public function editItem(Item $item)
{
$this->authorize('update', $item);
$categories = Item::distinct()->pluck('category')->filter()->values();
return view('inventory.item-form', compact('item', 'categories'));
}
public function updateItem(Request $request, Item $item)
{
$this->authorize('update', $item);
$validated = $request->validate([
'name' => 'required|string|max:255',
'code' => 'nullable|string|max:50|unique:items,code,' . $item->id,
'unit' => 'required|string|max:50',
'category' => 'nullable|string|max:100',
'description' => 'nullable|string',
'min_stock' => 'nullable|numeric|min:0',
'is_active' => 'boolean',
]);
$validated['is_active'] = $request->boolean('is_active');
$item->update($validated);
return redirect()
->route('inventory.items')
->with('success', __('inventory.item_updated'));
}
public function destroyItem(Item $item)
{
$this->authorize('delete', $item);
// Check if item has transactions
if ($item->inventoryTransactions()->count() > 0) {
return back()->withErrors(['item' => __('inventory.item_has_transactions')]);
}
$item->delete();
return redirect()
->route('inventory.items')
->with('success', __('inventory.item_deleted'));
}
// ==========================================
// WAREHOUSES CRUD
// ==========================================
public function warehouses(Request $request)
{
$query = Warehouse::with(['project']);
if ($request->filled('project_id')) {
$query->where('project_id', $request->project_id);
}
if ($request->filled('is_active')) {
$query->where('is_active', $request->boolean('is_active'));
}
$warehouses = $query->orderBy('name')->paginate(20)->appends($request->query());
$projects = Project::all();
return view('inventory.warehouses', compact('warehouses', 'projects'));
}
public function createWarehouse()
{
$this->authorize('create', Warehouse::class);
$warehouse = null;
$projects = Project::all();
return view('inventory.warehouse-form', compact('warehouse', 'projects'));
}
public function storeWarehouse(Request $request)
{
$this->authorize('create', Warehouse::class);
$validated = $request->validate([
'name' => 'required|string|max:255',
'location' => 'nullable|string|max:255',
'project_id' => 'nullable|exists:projects,id',
'is_active' => 'boolean',
]);
$validated['tenant_id'] = $this->getTenantId();
$validated['is_active'] = $request->boolean('is_active', true);
Warehouse::create($validated);
return redirect()
->route('inventory.warehouses')
->with('success', __('inventory.warehouse_created'));
}
public function editWarehouse(Warehouse $warehouse)
{
$this->authorize('update', $warehouse);
$projects = Project::all();
return view('inventory.warehouse-form', compact('warehouse', 'projects'));
}
public function updateWarehouse(Request $request, Warehouse $warehouse)
{
$this->authorize('update', $warehouse);
$validated = $request->validate([
'name' => 'required|string|max:255',
'location' => 'nullable|string|max:255',
'project_id' => 'nullable|exists:projects,id',
'is_active' => 'boolean',
]);
$validated['is_active'] = $request->boolean('is_active');
$warehouse->update($validated);
return redirect()
->route('inventory.warehouses')
->with('success', __('inventory.warehouse_updated'));
}
public function destroyWarehouse(Warehouse $warehouse)
{
$this->authorize('delete', $warehouse);
if ($warehouse->inventoryTransactions()->count() > 0) {
return back()->withErrors(['warehouse' => __('inventory.warehouse_has_transactions')]);
}
$warehouse->delete();
return redirect()
->route('inventory.warehouses')
->with('success', __('inventory.warehouse_deleted'));
}
// ==========================================
// TRANSACTIONS
// ==========================================
public function transactions(Request $request)
{
$query = InventoryTransaction::with(['item', 'warehouse', 'project', 'currency', 'createdBy']);
if ($request->filled('item_id')) { $query->where('item_id', $request->item_id); }
if ($request->filled('warehouse_id')) { $query->where('warehouse_id', $request->warehouse_id); }
if ($request->filled('type')) { $query->byType($request->type); }
if ($request->filled('project_id')) { $query->where('project_id', $request->project_id); }
if ($request->filled('from_date')) { $query->where('created_at', '>=', $request->from_date); }
if ($request->filled('to_date')) { $query->where('created_at', '<=', $request->to_date); }
$transactions = $query->orderBy('created_at', 'desc')->paginate(20)->appends($request->query());
$items = Item::where('is_active', true)->orderBy('name')->get();
$warehouses = Warehouse::where('is_active', true)->orderBy('name')->get();
$projects = Project::all();
$currencies = Currency::where('is_active', true)->get();
$types = ['in', 'out', 'transfer', 'return', 'adjustment'];
return view('inventory.transactions', compact(
'transactions', 'items', 'warehouses', 'projects', 'currencies', 'types'
));
}
public function stockIn(Request $request)
{
$validated = $request->validate([
'item_id' => 'required|exists:items,id',
'warehouse_id' => 'required|exists:warehouses,id',
'project_id' => 'nullable|exists:projects,id',
'quantity' => 'required|numeric|min:0.01',
'unit_price' => 'nullable|numeric|min:0',
'currency_id' => 'nullable|exists:currencies,id',
'reference_number' => 'nullable|string|max:100',
'notes' => 'nullable|string',
]);
$validated['tenant_id'] = $this->getTenantId();
$validated['type'] = 'in';
$validated['created_by'] = Auth::id();
if (!empty($validated['unit_price']) && !empty($validated['quantity'])) {
$validated['total_price'] = $validated['unit_price'] * $validated['quantity'];
}
InventoryTransaction::create($validated);
return redirect()
->route('inventory.transactions')
->with('success', __('inventory.stock_in_success'));
}
public function stockOut(Request $request)
{
$validated = $request->validate([
'item_id' => 'required|exists:items,id',
'warehouse_id' => 'required|exists:warehouses,id',
'project_id' => 'nullable|exists:projects,id',
'quantity' => 'required|numeric|min:0.01',
'unit_price' => 'nullable|numeric|min:0',
'currency_id' => 'nullable|exists:currencies,id',
'reference_number' => 'nullable|string|max:100',
'notes' => 'nullable|string',
]);
$currentStock = $this->getCurrentStockForItem($validated['item_id'], $validated['warehouse_id']);
if ($currentStock < $validated['quantity']) {
return back()
->withErrors(['quantity' => __('inventory.insufficient_stock')])
->withInput();
}
$validated['tenant_id'] = $this->getTenantId();
$validated['type'] = 'out';
$validated['created_by'] = Auth::id();
if (!empty($validated['unit_price']) && !empty($validated['quantity'])) {
$validated['total_price'] = $validated['unit_price'] * $validated['quantity'];
}
InventoryTransaction::create($validated);
return redirect()
->route('inventory.transactions')
->with('success', __('inventory.stock_out_success'));
}
public function stockTransfer(Request $request)
{
$validated = $request->validate([
'item_id' => 'required|exists:items,id',
'from_warehouse_id' => 'required|exists:warehouses,id',
'to_warehouse_id' => 'required|exists:warehouses,id|different:from_warehouse_id',
'project_id' => 'nullable|exists:projects,id',
'quantity' => 'required|numeric|min:0.01',
'notes' => 'nullable|string',
]);
// Check source stock
$currentStock = $this->getCurrentStockForItem($validated['item_id'], $validated['from_warehouse_id']);
if ($currentStock < $validated['quantity']) {
return back()
->withErrors(['quantity' => __('inventory.insufficient_stock_source')])
->withInput();
}
DB::transaction(function () use ($validated) {
$tenantId = $this->getTenantId();
$userId = Auth::id();
// Out from source
InventoryTransaction::create([
'tenant_id' => $tenantId,
'item_id' => $validated['item_id'],
'warehouse_id' => $validated['from_warehouse_id'],
'project_id' => $validated['project_id'] ?? null,
'type' => 'transfer',
'quantity' => $validated['quantity'],
'reference_number' => 'TRF-' . time(),
'notes' => ($validated['notes'] ?? '') . ' — انتقال به انبار مقصد',
'created_by' => $userId,
]);
// In to destination
InventoryTransaction::create([
'tenant_id' => $tenantId,
'item_id' => $validated['item_id'],
'warehouse_id' => $validated['to_warehouse_id'],
'project_id' => $validated['project_id'] ?? null,
'type' => 'transfer',
'quantity' => $validated['quantity'],
'reference_number' => 'TRF-' . time(),
'notes' => ($validated['notes'] ?? '') . ' — انتقال از انبار مبدأ',
'created_by' => $userId,
]);
});
return redirect()
->route('inventory.transactions')
->with('success', __('inventory.transfer_success'));
}
public function currentStock(Request $request)
{
$request->validate([
'item_id' => 'required|exists:items,id',
'warehouse_id' => 'nullable|exists:warehouses,id',
]);
$stock = $this->getCurrentStockForItem($request->item_id, $request->warehouse_id);
return response()->json([
'item_id' => $request->item_id,
'warehouse_id' => $request->warehouse_id,
'current_stock' => $stock,
]);
}
// ==========================================
// STOCK HELPERS
// ==========================================
private function getCurrentStockForItem(int $itemId, ?int $warehouseId = null): float
{
$query = InventoryTransaction::where('item_id', $itemId);
if ($warehouseId) {
$query->where('warehouse_id', $warehouseId);
}
$totalIn = (clone $query)->whereIn('type', ['in', 'return'])->sum('quantity');
$totalOut = (clone $query)->where('type', 'out')->sum('quantity');
$adjustments = (clone $query)->where('type', 'adjustment')->sum('quantity');
return (float) ($totalIn - $totalOut + $adjustments);
}
}