2025-12-30 15:03:19 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
2026-01-26 10:37:47 +08:00
|
|
|
|
namespace App\Modules\Inventory\Controllers;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
use Illuminate\Http\Request;
|
2026-01-26 14:59:24 +08:00
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
use Inertia\Inertia;
|
|
|
|
|
|
use App\Modules\Inventory\Models\Warehouse;
|
|
|
|
|
|
use App\Modules\Inventory\Models\Product;
|
|
|
|
|
|
use App\Modules\Inventory\Models\Inventory;
|
2026-01-26 10:37:47 +08:00
|
|
|
|
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
class InventoryController extends Controller
|
|
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
|
public function index(Request $request, Warehouse $warehouse)
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{
|
|
|
|
|
|
$warehouse->load([
|
|
|
|
|
|
'inventories.product.category',
|
2026-01-08 16:32:10 +08:00
|
|
|
|
'inventories.product.baseUnit',
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'inventories.lastIncomingTransaction',
|
|
|
|
|
|
'inventories.lastOutgoingTransaction'
|
|
|
|
|
|
]);
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$allProducts = Product::with('category')->get();
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 1. 準備 availableProducts
|
|
|
|
|
|
$availableProducts = $allProducts->map(function ($product) {
|
|
|
|
|
|
return [
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'id' => (string) $product->id,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'name' => $product->name,
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'type' => $product->category?->name ?? '其他',
|
2025-12-30 15:03:19 +08:00
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
|
// 2. 從新表格讀取安全庫存設定 (商品-倉庫層級)
|
|
|
|
|
|
$safetyStockMap = WarehouseProductSafetyStock::where('warehouse_id', $warehouse->id)
|
|
|
|
|
|
->pluck('safety_stock', 'product_id')
|
|
|
|
|
|
->mapWithKeys(fn($val, $key) => [(string)$key => (float)$val]);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 準備 inventories (批號分組)
|
|
|
|
|
|
$items = $warehouse->inventories()
|
|
|
|
|
|
->with(['product.baseUnit', 'lastIncomingTransaction', 'lastOutgoingTransaction'])
|
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
|
|
$inventories = $items->groupBy('product_id')->map(function ($batchItems) use ($safetyStockMap) {
|
|
|
|
|
|
$firstItem = $batchItems->first();
|
|
|
|
|
|
$product = $firstItem->product;
|
|
|
|
|
|
$totalQuantity = $batchItems->sum('quantity');
|
|
|
|
|
|
// 從獨立表格讀取安全庫存
|
|
|
|
|
|
$safetyStock = $safetyStockMap[(string)$firstItem->product_id] ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
// 計算狀態
|
|
|
|
|
|
$status = '正常';
|
|
|
|
|
|
if (!is_null($safetyStock)) {
|
|
|
|
|
|
if ($totalQuantity < $safetyStock) {
|
|
|
|
|
|
$status = '低於';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
return [
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'productId' => (string) $firstItem->product_id,
|
|
|
|
|
|
'productName' => $product?->name ?? '未知商品',
|
|
|
|
|
|
'productCode' => $product?->code ?? 'N/A',
|
|
|
|
|
|
'baseUnit' => $product?->baseUnit?->name ?? '個',
|
|
|
|
|
|
'totalQuantity' => (float) $totalQuantity,
|
|
|
|
|
|
'safetyStock' => $safetyStock,
|
|
|
|
|
|
'status' => $status,
|
|
|
|
|
|
'batches' => $batchItems->map(function ($inv) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $inv->id,
|
|
|
|
|
|
'warehouseId' => (string) $inv->warehouse_id,
|
|
|
|
|
|
'productId' => (string) $inv->product_id,
|
|
|
|
|
|
'productName' => $inv->product?->name ?? '未知商品',
|
|
|
|
|
|
'productCode' => $inv->product?->code ?? 'N/A',
|
|
|
|
|
|
'unit' => $inv->product?->baseUnit?->name ?? '個',
|
|
|
|
|
|
'quantity' => (float) $inv->quantity,
|
|
|
|
|
|
'safetyStock' => null, // 批號層級不再有安全庫存
|
|
|
|
|
|
'status' => '正常',
|
|
|
|
|
|
'batchNumber' => $inv->batch_number ?? 'BATCH-' . $inv->id,
|
|
|
|
|
|
'expiryDate' => $inv->expiry_date ? $inv->expiry_date->format('Y-m-d') : null,
|
|
|
|
|
|
'lastInboundDate' => $inv->lastIncomingTransaction ? ($inv->lastIncomingTransaction->actual_time ? $inv->lastIncomingTransaction->actual_time->format('Y-m-d') : $inv->lastIncomingTransaction->created_at->format('Y-m-d')) : null,
|
|
|
|
|
|
'lastOutboundDate' => $inv->lastOutgoingTransaction ? ($inv->lastOutgoingTransaction->actual_time ? $inv->lastOutgoingTransaction->actual_time->format('Y-m-d') : $inv->lastOutgoingTransaction->created_at->format('Y-m-d')) : null,
|
|
|
|
|
|
];
|
|
|
|
|
|
})->values(),
|
2025-12-30 15:03:19 +08:00
|
|
|
|
];
|
|
|
|
|
|
})->values();
|
|
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
|
// 4. 準備 safetyStockSettings (從新表格讀取)
|
|
|
|
|
|
$safetyStockSettings = WarehouseProductSafetyStock::where('warehouse_id', $warehouse->id)
|
|
|
|
|
|
->with(['product.category'])
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->map(function ($setting) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $setting->id,
|
|
|
|
|
|
'warehouseId' => (string) $setting->warehouse_id,
|
|
|
|
|
|
'productId' => (string) $setting->product_id,
|
|
|
|
|
|
'productName' => $setting->product?->name ?? '未知商品',
|
|
|
|
|
|
'productType' => $setting->product?->category?->name ?? '其他',
|
|
|
|
|
|
'safetyStock' => (float) $setting->safety_stock,
|
|
|
|
|
|
'createdAt' => $setting->created_at->toIso8601String(),
|
|
|
|
|
|
'updatedAt' => $setting->updated_at->toIso8601String(),
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
return Inertia::render('Warehouse/Inventory', [
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'warehouse' => $warehouse,
|
|
|
|
|
|
'inventories' => $inventories,
|
|
|
|
|
|
'safetyStockSettings' => $safetyStockSettings,
|
|
|
|
|
|
'availableProducts' => $availableProducts,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
public function create(Warehouse $warehouse)
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{
|
|
|
|
|
|
// 取得所有商品供前端選單使用
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$products = Product::with(['baseUnit', 'largeUnit'])
|
2026-01-22 15:39:35 +08:00
|
|
|
|
->select('id', 'name', 'code', 'base_unit_id', 'large_unit_id', 'conversion_rate')
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->map(function ($product) {
|
2025-12-30 15:03:19 +08:00
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $product->id,
|
|
|
|
|
|
'name' => $product->name,
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'code' => $product->code,
|
2026-01-08 16:32:10 +08:00
|
|
|
|
'baseUnit' => $product->baseUnit?->name ?? '個',
|
|
|
|
|
|
'largeUnit' => $product->largeUnit?->name, // 可能為 null
|
|
|
|
|
|
'conversionRate' => (float) $product->conversion_rate,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
return Inertia::render('Warehouse/AddInventory', [
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'warehouse' => $warehouse,
|
|
|
|
|
|
'products' => $products,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
public function store(Request $request, Warehouse $warehouse)
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{
|
|
|
|
|
|
$validated = $request->validate([
|
|
|
|
|
|
'inboundDate' => 'required|date',
|
|
|
|
|
|
'reason' => 'required|string',
|
|
|
|
|
|
'notes' => 'nullable|string',
|
|
|
|
|
|
'items' => 'required|array|min:1',
|
|
|
|
|
|
'items.*.productId' => 'required|exists:products,id',
|
|
|
|
|
|
'items.*.quantity' => 'required|numeric|min:0.01',
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'items.*.batchMode' => 'required|in:existing,new',
|
|
|
|
|
|
'items.*.inventoryId' => 'required_if:items.*.batchMode,existing|nullable|exists:inventories,id',
|
|
|
|
|
|
'items.*.originCountry' => 'required_if:items.*.batchMode,new|nullable|string|max:2',
|
2026-01-21 17:19:36 +08:00
|
|
|
|
'items.*.expiryDate' => 'nullable|date',
|
2025-12-30 15:03:19 +08:00
|
|
|
|
]);
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
return DB::transaction(function () use ($validated, $warehouse) {
|
2025-12-30 15:03:19 +08:00
|
|
|
|
foreach ($validated['items'] as $item) {
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$inventory = null;
|
|
|
|
|
|
|
|
|
|
|
|
if ($item['batchMode'] === 'existing') {
|
|
|
|
|
|
// 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加)
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$inventory = Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
2026-01-22 15:39:35 +08:00
|
|
|
|
if ($inventory->trashed()) {
|
|
|
|
|
|
$inventory->restore();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 模式 B:建立新批號
|
|
|
|
|
|
$originCountry = $item['originCountry'] ?? 'TW';
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$product = Product::find($item['productId']);
|
2026-01-22 15:39:35 +08:00
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$batchNumber = Inventory::generateBatchNumber(
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$product->code ?? 'UNK',
|
|
|
|
|
|
$originCountry,
|
|
|
|
|
|
$validated['inboundDate']
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 同樣要檢查此批號是否已經存在 (即使模式是 new, 但可能撞到同一天同產地手動建立的)
|
|
|
|
|
|
$inventory = $warehouse->inventories()->withTrashed()->firstOrNew(
|
|
|
|
|
|
[
|
|
|
|
|
|
'product_id' => $item['productId'],
|
|
|
|
|
|
'batch_number' => $batchNumber
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
'quantity' => 0,
|
|
|
|
|
|
'arrival_date' => $validated['inboundDate'],
|
|
|
|
|
|
'expiry_date' => $item['expiryDate'] ?? null,
|
|
|
|
|
|
'origin_country' => $originCountry,
|
|
|
|
|
|
]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if ($inventory->trashed()) {
|
|
|
|
|
|
$inventory->restore();
|
2026-01-21 17:19:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$currentQty = $inventory->quantity;
|
|
|
|
|
|
$newQty = $currentQty + $item['quantity'];
|
|
|
|
|
|
|
2026-01-19 11:47:10 +08:00
|
|
|
|
$inventory->quantity = $newQty;
|
|
|
|
|
|
$inventory->save();
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 寫入異動紀錄
|
|
|
|
|
|
$inventory->transactions()->create([
|
|
|
|
|
|
'type' => '手動入庫',
|
|
|
|
|
|
'quantity' => $item['quantity'],
|
|
|
|
|
|
'balance_before' => $currentQty,
|
|
|
|
|
|
'balance_after' => $newQty,
|
|
|
|
|
|
'reason' => $validated['reason'] . ($validated['notes'] ? ' - ' . $validated['notes'] : ''),
|
|
|
|
|
|
'actual_time' => $validated['inboundDate'],
|
|
|
|
|
|
'user_id' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return redirect()->route('warehouses.inventory.index', $warehouse->id)
|
|
|
|
|
|
->with('success', '庫存記錄已儲存成功');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* API: 取得商品在特定倉庫的所有批號,並回傳當前日期/產地下的一個流水號
|
|
|
|
|
|
*/
|
2026-01-26 14:59:24 +08:00
|
|
|
|
public function getBatches(Warehouse $warehouse, $productId, Request $request)
|
2026-01-22 15:39:35 +08:00
|
|
|
|
{
|
|
|
|
|
|
$originCountry = $request->query('originCountry', 'TW');
|
|
|
|
|
|
$arrivalDate = $request->query('arrivalDate', now()->format('Y-m-d'));
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$batches = Inventory::where('warehouse_id', $warehouse->id)
|
2026-01-22 15:39:35 +08:00
|
|
|
|
->where('product_id', $productId)
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->map(function ($inventory) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'inventoryId' => (string) $inventory->id,
|
|
|
|
|
|
'batchNumber' => $inventory->batch_number,
|
|
|
|
|
|
'originCountry' => $inventory->origin_country,
|
|
|
|
|
|
'expiryDate' => $inventory->expiry_date ? $inventory->expiry_date->format('Y-m-d') : null,
|
|
|
|
|
|
'quantity' => (float) $inventory->quantity,
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 計算下一個流水號
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$product = Product::find($productId);
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$nextSequence = '01';
|
|
|
|
|
|
if ($product) {
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$batchNumber = Inventory::generateBatchNumber(
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$product->code ?? 'UNK',
|
|
|
|
|
|
$originCountry,
|
|
|
|
|
|
$arrivalDate
|
|
|
|
|
|
);
|
|
|
|
|
|
$nextSequence = substr($batchNumber, -2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
|
'batches' => $batches,
|
|
|
|
|
|
'nextSequence' => $nextSequence
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
public function edit(Request $request, Warehouse $warehouse, $inventoryId)
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{
|
|
|
|
|
|
// 取得庫存紀錄,包含商品資訊與異動紀錄 (含經手人)
|
|
|
|
|
|
// 這裡如果是 Mock 的 ID (mock-inv-1),需要特殊處理
|
|
|
|
|
|
if (str_starts_with($inventoryId, 'mock-inv-')) {
|
|
|
|
|
|
return redirect()->back()->with('error', '無法編輯範例資料');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$inventory = Inventory::with(['product', 'transactions' => function($query) {
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
|
|
|
|
|
}, 'transactions.user'])->findOrFail($inventoryId);
|
|
|
|
|
|
|
|
|
|
|
|
// 轉換為前端需要的格式
|
|
|
|
|
|
$inventoryData = [
|
|
|
|
|
|
'id' => (string) $inventory->id,
|
|
|
|
|
|
'warehouseId' => (string) $inventory->warehouse_id,
|
|
|
|
|
|
'productId' => (string) $inventory->product_id,
|
2026-01-08 16:32:10 +08:00
|
|
|
|
'productName' => $inventory->product?->name ?? '未知商品',
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'quantity' => (float) $inventory->quantity,
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'batchNumber' => $inventory->batch_number ?? '-',
|
|
|
|
|
|
'expiryDate' => $inventory->expiry_date ?? null,
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'lastInboundDate' => $inventory->updated_at->format('Y-m-d'),
|
|
|
|
|
|
'lastOutboundDate' => null,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 整理異動紀錄
|
|
|
|
|
|
$transactions = $inventory->transactions->map(function ($tx) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $tx->id,
|
|
|
|
|
|
'type' => $tx->type,
|
|
|
|
|
|
'quantity' => (float) $tx->quantity,
|
|
|
|
|
|
'balanceAfter' => (float) $tx->balance_after,
|
|
|
|
|
|
'reason' => $tx->reason,
|
|
|
|
|
|
'userName' => $tx->user ? $tx->user->name : '系統',
|
|
|
|
|
|
'actualTime' => $tx->actual_time ? $tx->actual_time->format('Y-m-d H:i') : $tx->created_at->format('Y-m-d H:i'),
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
return Inertia::render('Warehouse/EditInventory', [
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'warehouse' => $warehouse,
|
|
|
|
|
|
'inventory' => $inventoryData,
|
|
|
|
|
|
'transactions' => $transactions,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
public function update(Request $request, Warehouse $warehouse, $inventoryId)
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{
|
|
|
|
|
|
// 若是 product ID (舊邏輯),先轉為 inventory
|
|
|
|
|
|
// 但新路由我們傳的是 inventory ID
|
|
|
|
|
|
// 為了相容,我們先判斷 $inventoryId 是 inventory ID
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$inventory = Inventory::find($inventoryId);
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果找不到 (可能是舊路由傳 product ID)
|
|
|
|
|
|
if (!$inventory) {
|
|
|
|
|
|
$inventory = $warehouse->inventories()->where('product_id', $inventoryId)->first();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!$inventory) {
|
|
|
|
|
|
return redirect()->back()->with('error', '找不到庫存紀錄');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$validated = $request->validate([
|
|
|
|
|
|
'quantity' => 'required|numeric|min:0',
|
|
|
|
|
|
// 以下欄位改為 nullable,支援新表單
|
|
|
|
|
|
'type' => 'nullable|string',
|
|
|
|
|
|
'operation' => 'nullable|in:add,subtract,set',
|
|
|
|
|
|
'reason' => 'nullable|string',
|
|
|
|
|
|
'notes' => 'nullable|string',
|
|
|
|
|
|
// 新增日期欄位驗證 (雖然暫不儲存到 DB)
|
|
|
|
|
|
'batchNumber' => 'nullable|string',
|
|
|
|
|
|
'expiryDate' => 'nullable|date',
|
|
|
|
|
|
'lastInboundDate' => 'nullable|date',
|
|
|
|
|
|
'lastOutboundDate' => 'nullable|date',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
return DB::transaction(function () use ($validated, $inventory) {
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$currentQty = (float) $inventory->quantity;
|
|
|
|
|
|
$newQty = (float) $validated['quantity'];
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
|
// 判斷是否來自調整彈窗 (包含 operation 參數)
|
|
|
|
|
|
$isAdjustment = isset($validated['operation']);
|
|
|
|
|
|
$changeQty = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if ($isAdjustment) {
|
2025-12-30 15:03:19 +08:00
|
|
|
|
switch ($validated['operation']) {
|
|
|
|
|
|
case 'add':
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$changeQty = (float) $validated['quantity'];
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$newQty = $currentQty + $changeQty;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'subtract':
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$changeQty = -(float) $validated['quantity'];
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$newQty = $currentQty + $changeQty;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'set':
|
|
|
|
|
|
$changeQty = $newQty - $currentQty;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 來自編輯頁面,直接 Set
|
|
|
|
|
|
$changeQty = $newQty - $currentQty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新庫存
|
|
|
|
|
|
$inventory->update(['quantity' => $newQty]);
|
|
|
|
|
|
|
|
|
|
|
|
// 異動類型映射
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$type = $validated['type'] ?? ($isAdjustment ? 'manual_adjustment' : 'adjustment');
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$typeMapping = [
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'manual_adjustment' => '手動調整庫存',
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'adjustment' => '盤點調整',
|
|
|
|
|
|
'purchase_in' => '採購進貨',
|
|
|
|
|
|
'sales_out' => '銷售出庫',
|
|
|
|
|
|
'return_in' => '退貨入庫',
|
|
|
|
|
|
'return_out' => '退貨出庫',
|
|
|
|
|
|
'transfer_in' => '撥補入庫',
|
|
|
|
|
|
'transfer_out' => '撥補出庫',
|
|
|
|
|
|
];
|
|
|
|
|
|
$chineseType = $typeMapping[$type] ?? $type;
|
|
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
|
// 如果是編輯頁面來的,且沒傳 type,設為手動編輯
|
|
|
|
|
|
if (!$isAdjustment && !isset($validated['type'])) {
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$chineseType = '手動編輯';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
|
// 整理原因
|
|
|
|
|
|
$reason = $validated['reason'] ?? ($isAdjustment ? '手動庫存調整' : '編輯頁面更新');
|
|
|
|
|
|
if (isset($validated['notes'])) {
|
|
|
|
|
|
$reason .= ' - ' . $validated['notes'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 15:03:19 +08:00
|
|
|
|
// 寫入異動紀錄
|
|
|
|
|
|
if (abs($changeQty) > 0.0001) {
|
|
|
|
|
|
$inventory->transactions()->create([
|
|
|
|
|
|
'type' => $chineseType,
|
|
|
|
|
|
'quantity' => $changeQty,
|
|
|
|
|
|
'balance_before' => $currentQty,
|
|
|
|
|
|
'balance_after' => $newQty,
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'reason' => $reason,
|
|
|
|
|
|
'actual_time' => now(),
|
2025-12-30 15:03:19 +08:00
|
|
|
|
'user_id' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return redirect()->route('warehouses.inventory.index', $inventory->warehouse_id)
|
|
|
|
|
|
->with('success', '庫存資料已更新');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
public function destroy(Warehouse $warehouse, $inventoryId)
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$inventory = Inventory::findOrFail($inventoryId);
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
|
// 庫存 > 0 不允許刪除 (哪怕是軟刪除)
|
2025-12-30 15:03:19 +08:00
|
|
|
|
if ($inventory->quantity > 0) {
|
2026-01-22 15:39:35 +08:00
|
|
|
|
return redirect()->back()->with('error', '庫存數量大於 0,無法刪除。請先進行出庫或調整。');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 歸零異動 (因為已經限制為 0 才能刪,這段邏輯可以簡化,但為了保險起見,若有微小殘值仍可記錄歸零)
|
|
|
|
|
|
if (abs($inventory->quantity) > 0.0001) {
|
2025-12-30 15:03:19 +08:00
|
|
|
|
$inventory->transactions()->create([
|
|
|
|
|
|
'type' => '手動編輯',
|
|
|
|
|
|
'quantity' => -$inventory->quantity,
|
|
|
|
|
|
'balance_before' => $inventory->quantity,
|
|
|
|
|
|
'balance_after' => 0,
|
|
|
|
|
|
'reason' => '刪除庫存品項',
|
|
|
|
|
|
'actual_time' => now(),
|
|
|
|
|
|
'user_id' => auth()->id(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$inventory->delete();
|
|
|
|
|
|
|
|
|
|
|
|
return redirect()->route('warehouses.inventory.index', $warehouse->id)
|
|
|
|
|
|
->with('success', '庫存品項已刪除');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 10:37:47 +08:00
|
|
|
|
public function history(Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse)
|
2025-12-30 15:03:19 +08:00
|
|
|
|
{
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$inventoryId = $request->query('inventoryId');
|
|
|
|
|
|
$productId = $request->query('productId');
|
|
|
|
|
|
|
|
|
|
|
|
if ($productId) {
|
|
|
|
|
|
// 商品層級查詢
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$inventories = Inventory::where('warehouse_id', $warehouse->id)
|
2026-01-22 15:39:35 +08:00
|
|
|
|
->where('product_id', $productId)
|
|
|
|
|
|
->with(['product', 'transactions' => function($query) {
|
|
|
|
|
|
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
|
|
|
|
|
}, 'transactions.user'])
|
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
|
|
if ($inventories->isEmpty()) {
|
|
|
|
|
|
return redirect()->back()->with('error', '找不到該商品的庫存紀錄');
|
|
|
|
|
|
}
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$firstInventory = $inventories->first();
|
|
|
|
|
|
$productName = $firstInventory->product?->name ?? '未知商品';
|
|
|
|
|
|
$productCode = $firstInventory->product?->code ?? 'N/A';
|
|
|
|
|
|
$currentTotalQuantity = $inventories->sum('quantity');
|
|
|
|
|
|
|
|
|
|
|
|
// 合併所有批號的交易紀錄
|
|
|
|
|
|
$allTransactions = collect();
|
|
|
|
|
|
foreach ($inventories as $inv) {
|
|
|
|
|
|
foreach ($inv->transactions as $tx) {
|
|
|
|
|
|
$allTransactions->push([
|
|
|
|
|
|
'raw_tx' => $tx,
|
|
|
|
|
|
'batchNumber' => $inv->batch_number ?? '-',
|
|
|
|
|
|
'sort_time' => $tx->actual_time ?? $tx->created_at,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-30 15:03:19 +08:00
|
|
|
|
|
2026-01-22 15:39:35 +08:00
|
|
|
|
// 依時間倒序排序 (最新的在前面)
|
|
|
|
|
|
$sortedTransactions = $allTransactions->sort(function ($a, $b) {
|
|
|
|
|
|
// 先比時間 (Desc)
|
|
|
|
|
|
if ($a['sort_time'] != $b['sort_time']) {
|
|
|
|
|
|
return $a['sort_time'] > $b['sort_time'] ? -1 : 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 再比 ID (Desc)
|
|
|
|
|
|
return $a['raw_tx']->id > $b['raw_tx']->id ? -1 : 1;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 回推計算結餘
|
|
|
|
|
|
$runningBalance = $currentTotalQuantity;
|
|
|
|
|
|
$transactions = $sortedTransactions->map(function ($item) use (&$runningBalance) {
|
|
|
|
|
|
$tx = $item['raw_tx'];
|
|
|
|
|
|
|
|
|
|
|
|
// 本次異動後的結餘 = 當前推算的結餘
|
|
|
|
|
|
$balanceAfter = $runningBalance;
|
|
|
|
|
|
|
|
|
|
|
|
// 推算前一次的結餘 (減去本次的異動量:如果是入庫+10,前一次就是-10)
|
|
|
|
|
|
$runningBalance = $runningBalance - $tx->quantity;
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $tx->id,
|
|
|
|
|
|
'type' => $tx->type,
|
|
|
|
|
|
'quantity' => (float) $tx->quantity,
|
|
|
|
|
|
'balanceAfter' => (float) $balanceAfter, // 使用即時計算的商品總結餘
|
|
|
|
|
|
'reason' => $tx->reason,
|
|
|
|
|
|
'userName' => $tx->user ? $tx->user->name : '系統',
|
|
|
|
|
|
'actualTime' => $tx->actual_time ? $tx->actual_time->format('Y-m-d H:i') : $tx->created_at->format('Y-m-d H:i'),
|
|
|
|
|
|
'batchNumber' => $item['batchNumber'],
|
|
|
|
|
|
];
|
|
|
|
|
|
})->values();
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
return Inertia::render('Warehouse/InventoryHistory', [
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'warehouse' => $warehouse,
|
|
|
|
|
|
'inventory' => [
|
|
|
|
|
|
'id' => 'product-' . $productId,
|
|
|
|
|
|
'productName' => $productName,
|
|
|
|
|
|
'productCode' => $productCode,
|
|
|
|
|
|
'quantity' => (float) $currentTotalQuantity,
|
|
|
|
|
|
],
|
|
|
|
|
|
'transactions' => $transactions
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($inventoryId) {
|
|
|
|
|
|
// 單一批號查詢
|
2026-01-26 14:59:24 +08:00
|
|
|
|
$inventory = Inventory::with(['product', 'transactions' => function($query) {
|
2026-01-22 15:39:35 +08:00
|
|
|
|
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
|
|
|
|
|
}, 'transactions.user'])->findOrFail($inventoryId);
|
|
|
|
|
|
|
|
|
|
|
|
$transactions = $inventory->transactions->map(function ($tx) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'id' => (string) $tx->id,
|
|
|
|
|
|
'type' => $tx->type,
|
|
|
|
|
|
'quantity' => (float) $tx->quantity,
|
|
|
|
|
|
'balanceAfter' => (float) $tx->balance_after,
|
|
|
|
|
|
'reason' => $tx->reason,
|
|
|
|
|
|
'userName' => $tx->user ? $tx->user->name : '系統',
|
|
|
|
|
|
'actualTime' => $tx->actual_time ? $tx->actual_time->format('Y-m-d H:i') : $tx->created_at->format('Y-m-d H:i'),
|
|
|
|
|
|
];
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-26 14:59:24 +08:00
|
|
|
|
return Inertia::render('Warehouse/InventoryHistory', [
|
2026-01-22 15:39:35 +08:00
|
|
|
|
'warehouse' => $warehouse,
|
|
|
|
|
|
'inventory' => [
|
|
|
|
|
|
'id' => (string) $inventory->id,
|
|
|
|
|
|
'productName' => $inventory->product?->name ?? '未知商品',
|
|
|
|
|
|
'productCode' => $inventory->product?->code ?? 'N/A',
|
|
|
|
|
|
'batchNumber' => $inventory->batch_number ?? '-',
|
|
|
|
|
|
'quantity' => (float) $inventory->quantity,
|
|
|
|
|
|
],
|
|
|
|
|
|
'transactions' => $transactions
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return redirect()->back()->with('error', '未提供查詢參數');
|
2025-12-30 15:03:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|