feat: 修正庫存與撥補單邏輯並整合文件
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 53s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

1. 修復倉庫統計數據加總與樣式。
2. 修正可用庫存計算邏輯(排除不可銷售倉庫)。
3. 撥補單商品列表加入批號與效期顯示。
4. 修正撥補單儲存邏輯以支援精確批號轉移。
5. 整合 FEATURES.md 至 README.md。
This commit is contained in:
2026-01-26 14:59:24 +08:00
parent b0848a6bb8
commit 106de4e945
81 changed files with 4118 additions and 1023 deletions

View File

@@ -5,11 +5,16 @@ namespace App\Modules\Inventory\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
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;
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
class InventoryController extends Controller
{
public function index(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse)
public function index(Request $request, Warehouse $warehouse)
{
$warehouse->load([
'inventories.product.category',
@@ -17,7 +22,7 @@ class InventoryController extends Controller
'inventories.lastIncomingTransaction',
'inventories.lastOutgoingTransaction'
]);
$allProducts = \App\Modules\Inventory\Models\Product::with('category')->get();
$allProducts = Product::with('category')->get();
// 1. 準備 availableProducts
$availableProducts = $allProducts->map(function ($product) {
@@ -98,7 +103,7 @@ class InventoryController extends Controller
];
});
return \Inertia\Inertia::render('Warehouse/Inventory', [
return Inertia::render('Warehouse/Inventory', [
'warehouse' => $warehouse,
'inventories' => $inventories,
'safetyStockSettings' => $safetyStockSettings,
@@ -106,10 +111,10 @@ class InventoryController extends Controller
]);
}
public function create(\App\Modules\Inventory\Models\Warehouse $warehouse)
public function create(Warehouse $warehouse)
{
// 取得所有商品供前端選單使用
$products = \App\Modules\Inventory\Models\Product::with(['baseUnit', 'largeUnit'])
$products = Product::with(['baseUnit', 'largeUnit'])
->select('id', 'name', 'code', 'base_unit_id', 'large_unit_id', 'conversion_rate')
->get()
->map(function ($product) {
@@ -123,13 +128,13 @@ class InventoryController extends Controller
];
});
return \Inertia\Inertia::render('Warehouse/AddInventory', [
return Inertia::render('Warehouse/AddInventory', [
'warehouse' => $warehouse,
'products' => $products,
]);
}
public function store(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse)
public function store(Request $request, Warehouse $warehouse)
{
$validated = $request->validate([
'inboundDate' => 'required|date',
@@ -144,22 +149,22 @@ class InventoryController extends Controller
'items.*.expiryDate' => 'nullable|date',
]);
return \Illuminate\Support\Facades\DB::transaction(function () use ($validated, $warehouse) {
return DB::transaction(function () use ($validated, $warehouse) {
foreach ($validated['items'] as $item) {
$inventory = null;
if ($item['batchMode'] === 'existing') {
// 模式 A選擇現有批號 (包含已刪除的也要能找回來累加)
$inventory = \App\Modules\Inventory\Models\Inventory::withTrashed()->findOrFail($item['inventoryId']);
$inventory = Inventory::withTrashed()->findOrFail($item['inventoryId']);
if ($inventory->trashed()) {
$inventory->restore();
}
} else {
// 模式 B建立新批號
$originCountry = $item['originCountry'] ?? 'TW';
$product = \App\Modules\Inventory\Models\Product::find($item['productId']);
$product = Product::find($item['productId']);
$batchNumber = \App\Modules\Inventory\Models\Inventory::generateBatchNumber(
$batchNumber = Inventory::generateBatchNumber(
$product->code ?? 'UNK',
$originCountry,
$validated['inboundDate']
@@ -210,12 +215,12 @@ class InventoryController extends Controller
/**
* API: 取得商品在特定倉庫的所有批號,並回傳當前日期/產地下的一個流水號
*/
public function getBatches(\App\Modules\Inventory\Models\Warehouse $warehouse, $productId, \Illuminate\Http\Request $request)
public function getBatches(Warehouse $warehouse, $productId, Request $request)
{
$originCountry = $request->query('originCountry', 'TW');
$arrivalDate = $request->query('arrivalDate', now()->format('Y-m-d'));
$batches = \App\Modules\Inventory\Models\Inventory::where('warehouse_id', $warehouse->id)
$batches = Inventory::where('warehouse_id', $warehouse->id)
->where('product_id', $productId)
->get()
->map(function ($inventory) {
@@ -229,10 +234,10 @@ class InventoryController extends Controller
});
// 計算下一個流水號
$product = \App\Modules\Inventory\Models\Product::find($productId);
$product = Product::find($productId);
$nextSequence = '01';
if ($product) {
$batchNumber = \App\Modules\Inventory\Models\Inventory::generateBatchNumber(
$batchNumber = Inventory::generateBatchNumber(
$product->code ?? 'UNK',
$originCountry,
$arrivalDate
@@ -246,7 +251,7 @@ class InventoryController extends Controller
]);
}
public function edit(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse, $inventoryId)
public function edit(Request $request, Warehouse $warehouse, $inventoryId)
{
// 取得庫存紀錄,包含商品資訊與異動紀錄 (含經手人)
// 這裡如果是 Mock 的 ID (mock-inv-1),需要特殊處理
@@ -254,7 +259,7 @@ class InventoryController extends Controller
return redirect()->back()->with('error', '無法編輯範例資料');
}
$inventory = \App\Modules\Inventory\Models\Inventory::with(['product', 'transactions' => function($query) {
$inventory = Inventory::with(['product', 'transactions' => function($query) {
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
}, 'transactions.user'])->findOrFail($inventoryId);
@@ -284,20 +289,20 @@ class InventoryController extends Controller
];
});
return \Inertia\Inertia::render('Warehouse/EditInventory', [
return Inertia::render('Warehouse/EditInventory', [
'warehouse' => $warehouse,
'inventory' => $inventoryData,
'transactions' => $transactions,
]);
}
public function update(\Illuminate\Http\Request $request, \App\Modules\Inventory\Models\Warehouse $warehouse, $inventoryId)
public function update(Request $request, Warehouse $warehouse, $inventoryId)
{
// 若是 product ID (舊邏輯),先轉為 inventory
// 但新路由我們傳的是 inventory ID
// 為了相容,我們先判斷 $inventoryId 是 inventory ID
$inventory = \App\Modules\Inventory\Models\Inventory::find($inventoryId);
$inventory = Inventory::find($inventoryId);
// 如果找不到 (可能是舊路由傳 product ID)
if (!$inventory) {
@@ -322,7 +327,7 @@ class InventoryController extends Controller
'lastOutboundDate' => 'nullable|date',
]);
return \Illuminate\Support\Facades\DB::transaction(function () use ($validated, $inventory) {
return DB::transaction(function () use ($validated, $inventory) {
$currentQty = (float) $inventory->quantity;
$newQty = (float) $validated['quantity'];
@@ -395,9 +400,9 @@ class InventoryController extends Controller
});
}
public function destroy(\App\Modules\Inventory\Models\Warehouse $warehouse, $inventoryId)
public function destroy(Warehouse $warehouse, $inventoryId)
{
$inventory = \App\Modules\Inventory\Models\Inventory::findOrFail($inventoryId);
$inventory = Inventory::findOrFail($inventoryId);
// 庫存 > 0 不允許刪除 (哪怕是軟刪除)
if ($inventory->quantity > 0) {
@@ -430,7 +435,7 @@ class InventoryController extends Controller
if ($productId) {
// 商品層級查詢
$inventories = \App\Modules\Inventory\Models\Inventory::where('warehouse_id', $warehouse->id)
$inventories = Inventory::where('warehouse_id', $warehouse->id)
->where('product_id', $productId)
->with(['product', 'transactions' => function($query) {
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
@@ -491,7 +496,7 @@ class InventoryController extends Controller
];
})->values();
return \Inertia\Inertia::render('Warehouse/InventoryHistory', [
return Inertia::render('Warehouse/InventoryHistory', [
'warehouse' => $warehouse,
'inventory' => [
'id' => 'product-' . $productId,
@@ -505,7 +510,7 @@ class InventoryController extends Controller
if ($inventoryId) {
// 單一批號查詢
$inventory = \App\Modules\Inventory\Models\Inventory::with(['product', 'transactions' => function($query) {
$inventory = Inventory::with(['product', 'transactions' => function($query) {
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
}, 'transactions.user'])->findOrFail($inventoryId);
@@ -521,7 +526,7 @@ class InventoryController extends Controller
];
});
return \Inertia\Inertia::render('Warehouse/InventoryHistory', [
return Inertia::render('Warehouse/InventoryHistory', [
'warehouse' => $warehouse,
'inventory' => [
'id' => (string) $inventory->id,