生產工單BOM以及批號完善
This commit is contained in:
@@ -7,8 +7,10 @@ use App\Models\Vendor;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\Warehouse;
|
||||
use App\Models\Inventory;
|
||||
use App\Models\WarehouseProductSafetyStock;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
@@ -21,15 +23,25 @@ class DashboardController extends Controller
|
||||
return redirect()->route('landlord.dashboard');
|
||||
}
|
||||
|
||||
// 計算低庫存數量:各商品在各倉庫的總量 < 安全庫存
|
||||
$lowStockCount = DB::table('warehouse_product_safety_stocks as ss')
|
||||
->join(DB::raw('(SELECT warehouse_id, product_id, SUM(quantity) as total_qty FROM inventories WHERE deleted_at IS NULL GROUP BY warehouse_id, product_id) as inv'),
|
||||
function ($join) {
|
||||
$join->on('ss.warehouse_id', '=', 'inv.warehouse_id')
|
||||
->on('ss.product_id', '=', 'inv.product_id');
|
||||
})
|
||||
->whereRaw('inv.total_qty <= ss.safety_stock')
|
||||
->count();
|
||||
|
||||
$stats = [
|
||||
'productsCount' => Product::count(),
|
||||
'vendorsCount' => Vendor::count(),
|
||||
'purchaseOrdersCount' => PurchaseOrder::count(),
|
||||
'warehousesCount' => Warehouse::count(),
|
||||
'totalInventoryValue' => Inventory::join('products', 'inventories.product_id', '=', 'products.id')
|
||||
->sum('inventories.quantity'), // Simplified, maybe just sum quantities for now
|
||||
->sum('inventories.quantity'),
|
||||
'pendingOrdersCount' => PurchaseOrder::where('status', 'pending')->count(),
|
||||
'lowStockCount' => Inventory::whereColumn('quantity', '<=', 'safety_stock')->count(),
|
||||
'lowStockCount' => $lowStockCount,
|
||||
];
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\WarehouseProductSafetyStock;
|
||||
|
||||
class InventoryController extends Controller
|
||||
{
|
||||
@@ -19,49 +20,82 @@ class InventoryController extends Controller
|
||||
// 1. 準備 availableProducts
|
||||
$availableProducts = $allProducts->map(function ($product) {
|
||||
return [
|
||||
'id' => (string) $product->id, // Frontend expects string
|
||||
'id' => (string) $product->id,
|
||||
'name' => $product->name,
|
||||
'type' => $product->category?->name ?? '其他', // 暫時用 Category Name 當 Type
|
||||
'type' => $product->category?->name ?? '其他',
|
||||
];
|
||||
});
|
||||
|
||||
// 2. 準備 inventories (模擬批號)
|
||||
// 2. 準備 inventories
|
||||
// 資料庫結構為 (warehouse_id, product_id) 唯一,故為扁平列表
|
||||
$inventories = $warehouse->inventories->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' => $inv->safety_stock !== null ? (float) $inv->safety_stock : null,
|
||||
'status' => '正常', // 前端會根據 quantity 與 safetyStock 重算,但後端亦可提供基礎狀態
|
||||
'batchNumber' => $inv->batch_number ?? 'BATCH-' . $inv->id, // 優先使用 DB 批號,若無則 fallback
|
||||
'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,
|
||||
];
|
||||
});
|
||||
// 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 = '低於';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 準備 safetyStockSettings
|
||||
$safetyStockSettings = $warehouse->inventories->filter(function($inv) {
|
||||
return !is_null($inv->safety_stock);
|
||||
})->map(function ($inv) {
|
||||
return [
|
||||
'id' => 'ss-' . $inv->id,
|
||||
'warehouseId' => (string) $inv->warehouse_id,
|
||||
'productId' => (string) $inv->product_id,
|
||||
'productName' => $inv->product?->name ?? '未知商品',
|
||||
'productType' => $inv->product?->category?->name ?? '其他',
|
||||
'safetyStock' => (float) $inv->safety_stock,
|
||||
'createdAt' => $inv->created_at->toIso8601String(),
|
||||
'updatedAt' => $inv->updated_at->toIso8601String(),
|
||||
'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(),
|
||||
];
|
||||
})->values();
|
||||
|
||||
// 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(),
|
||||
];
|
||||
});
|
||||
|
||||
return \Inertia\Inertia::render('Warehouse/Inventory', [
|
||||
'warehouse' => $warehouse,
|
||||
'inventories' => $inventories,
|
||||
@@ -73,10 +107,14 @@ class InventoryController extends Controller
|
||||
public function create(\App\Models\Warehouse $warehouse)
|
||||
{
|
||||
// 取得所有商品供前端選單使用
|
||||
$products = \App\Models\Product::with(['baseUnit', 'largeUnit'])->select('id', 'name', 'base_unit_id', 'large_unit_id', 'conversion_rate')->get()->map(function ($product) {
|
||||
$products = \App\Models\Product::with(['baseUnit', 'largeUnit'])
|
||||
->select('id', 'name', 'code', 'base_unit_id', 'large_unit_id', 'conversion_rate')
|
||||
->get()
|
||||
->map(function ($product) {
|
||||
return [
|
||||
'id' => (string) $product->id,
|
||||
'name' => $product->name,
|
||||
'code' => $product->code,
|
||||
'baseUnit' => $product->baseUnit?->name ?? '個',
|
||||
'largeUnit' => $product->largeUnit?->name, // 可能為 null
|
||||
'conversionRate' => (float) $product->conversion_rate,
|
||||
@@ -98,45 +136,55 @@ class InventoryController extends Controller
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.productId' => 'required|exists:products,id',
|
||||
'items.*.quantity' => 'required|numeric|min:0.01',
|
||||
'items.*.batchNumber' => 'nullable|string',
|
||||
'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',
|
||||
'items.*.expiryDate' => 'nullable|date',
|
||||
]);
|
||||
|
||||
return \Illuminate\Support\Facades\DB::transaction(function () use ($validated, $warehouse) {
|
||||
foreach ($validated['items'] as $item) {
|
||||
$batchNumber = $item['batchNumber'] ?? null;
|
||||
// 如果未提供批號,且系統設定需要批號,則自動產生 (這裡先保留彈性,若無則為 null 或預設)
|
||||
if (empty($batchNumber)) {
|
||||
// 嘗試自動產生:需要 product_code, country, date
|
||||
$inventory = null;
|
||||
|
||||
if ($item['batchMode'] === 'existing') {
|
||||
// 模式 A:選擇現有批號 (包含已刪除的也要能找回來累加)
|
||||
$inventory = \App\Models\Inventory::withTrashed()->findOrFail($item['inventoryId']);
|
||||
if ($inventory->trashed()) {
|
||||
$inventory->restore();
|
||||
}
|
||||
} else {
|
||||
// 模式 B:建立新批號
|
||||
$originCountry = $item['originCountry'] ?? 'TW';
|
||||
$product = \App\Models\Product::find($item['productId']);
|
||||
if ($product) {
|
||||
$batchNumber = \App\Models\Inventory::generateBatchNumber(
|
||||
$product->code ?? 'UNK',
|
||||
'TW', // 預設來源
|
||||
$validated['inboundDate']
|
||||
);
|
||||
|
||||
$batchNumber = \App\Models\Inventory::generateBatchNumber(
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
||||
// 取得或建立庫存紀錄 (加入批號判斷)
|
||||
$inventory = $warehouse->inventories()->firstOrNew(
|
||||
[
|
||||
'product_id' => $item['productId'],
|
||||
'batch_number' => $batchNumber
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'safety_stock' => null,
|
||||
'arrival_date' => $validated['inboundDate'],
|
||||
'expiry_date' => $item['expiryDate'] ?? null,
|
||||
'origin_country' => 'TW', // 預設
|
||||
]
|
||||
);
|
||||
|
||||
$currentQty = $inventory->quantity;
|
||||
$newQty = $currentQty + $item['quantity'];
|
||||
|
||||
// 更新庫存並儲存 (新紀錄: Created, 舊紀錄: Updated)
|
||||
$inventory->quantity = $newQty;
|
||||
$inventory->save();
|
||||
|
||||
@@ -157,7 +205,46 @@ class InventoryController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
public function edit(\App\Models\Warehouse $warehouse, $inventoryId)
|
||||
/**
|
||||
* API: 取得商品在特定倉庫的所有批號,並回傳當前日期/產地下的一個流水號
|
||||
*/
|
||||
public function getBatches(\App\Models\Warehouse $warehouse, $productId, \Illuminate\Http\Request $request)
|
||||
{
|
||||
$originCountry = $request->query('originCountry', 'TW');
|
||||
$arrivalDate = $request->query('arrivalDate', now()->format('Y-m-d'));
|
||||
|
||||
$batches = \App\Models\Inventory::where('warehouse_id', $warehouse->id)
|
||||
->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,
|
||||
];
|
||||
});
|
||||
|
||||
// 計算下一個流水號
|
||||
$product = \App\Models\Product::find($productId);
|
||||
$nextSequence = '01';
|
||||
if ($product) {
|
||||
$batchNumber = \App\Models\Inventory::generateBatchNumber(
|
||||
$product->code ?? 'UNK',
|
||||
$originCountry,
|
||||
$arrivalDate
|
||||
);
|
||||
$nextSequence = substr($batchNumber, -2);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'batches' => $batches,
|
||||
'nextSequence' => $nextSequence
|
||||
]);
|
||||
}
|
||||
|
||||
public function edit(\Illuminate\Http\Request $request, \App\Models\Warehouse $warehouse, $inventoryId)
|
||||
{
|
||||
// 取得庫存紀錄,包含商品資訊與異動紀錄 (含經手人)
|
||||
// 這裡如果是 Mock 的 ID (mock-inv-1),需要特殊處理
|
||||
@@ -176,8 +263,8 @@ class InventoryController extends Controller
|
||||
'productId' => (string) $inventory->product_id,
|
||||
'productName' => $inventory->product?->name ?? '未知商品',
|
||||
'quantity' => (float) $inventory->quantity,
|
||||
'batchNumber' => 'BATCH-' . $inventory->id, // Mock
|
||||
'expiryDate' => '2099-12-31', // Mock
|
||||
'batchNumber' => $inventory->batch_number ?? '-',
|
||||
'expiryDate' => $inventory->expiry_date ?? null,
|
||||
'lastInboundDate' => $inventory->updated_at->format('Y-m-d'),
|
||||
'lastOutboundDate' => null,
|
||||
];
|
||||
@@ -234,19 +321,21 @@ class InventoryController extends Controller
|
||||
]);
|
||||
|
||||
return \Illuminate\Support\Facades\DB::transaction(function () use ($validated, $inventory) {
|
||||
$currentQty = $inventory->quantity;
|
||||
$newQty = $validated['quantity'];
|
||||
$currentQty = (float) $inventory->quantity;
|
||||
$newQty = (float) $validated['quantity'];
|
||||
|
||||
// 判斷操作模式
|
||||
if (isset($validated['operation'])) {
|
||||
$changeQty = 0;
|
||||
// 判斷是否來自調整彈窗 (包含 operation 參數)
|
||||
$isAdjustment = isset($validated['operation']);
|
||||
$changeQty = 0;
|
||||
|
||||
if ($isAdjustment) {
|
||||
switch ($validated['operation']) {
|
||||
case 'add':
|
||||
$changeQty = $validated['quantity'];
|
||||
$changeQty = (float) $validated['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'subtract':
|
||||
$changeQty = -$validated['quantity'];
|
||||
$changeQty = -(float) $validated['quantity'];
|
||||
$newQty = $currentQty + $changeQty;
|
||||
break;
|
||||
case 'set':
|
||||
@@ -262,8 +351,9 @@ class InventoryController extends Controller
|
||||
$inventory->update(['quantity' => $newQty]);
|
||||
|
||||
// 異動類型映射
|
||||
$type = $validated['type'] ?? 'adjustment';
|
||||
$type = $validated['type'] ?? ($isAdjustment ? 'manual_adjustment' : 'adjustment');
|
||||
$typeMapping = [
|
||||
'manual_adjustment' => '手動調整庫存',
|
||||
'adjustment' => '盤點調整',
|
||||
'purchase_in' => '採購進貨',
|
||||
'sales_out' => '銷售出庫',
|
||||
@@ -274,22 +364,26 @@ class InventoryController extends Controller
|
||||
];
|
||||
$chineseType = $typeMapping[$type] ?? $type;
|
||||
|
||||
// 如果是編輯頁面來的,可能沒有 type,預設為 "盤點調整" 或 "手動編輯"
|
||||
if (!isset($validated['type'])) {
|
||||
// 如果是編輯頁面來的,且沒傳 type,設為手動編輯
|
||||
if (!$isAdjustment && !isset($validated['type'])) {
|
||||
$chineseType = '手動編輯';
|
||||
}
|
||||
|
||||
// 整理原因
|
||||
$reason = $validated['reason'] ?? ($isAdjustment ? '手動庫存調整' : '編輯頁面更新');
|
||||
if (isset($validated['notes'])) {
|
||||
$reason .= ' - ' . $validated['notes'];
|
||||
}
|
||||
|
||||
// 寫入異動紀錄
|
||||
// 如果數量沒變,是否要寫紀錄?通常編輯頁面按儲存可能只改了其他欄位(如果有)
|
||||
// 但因為我們目前只存 quantity,如果 quantity 沒變,可以不寫異動,或者寫一筆 0 的異動代表更新屬性
|
||||
if (abs($changeQty) > 0.0001) {
|
||||
$inventory->transactions()->create([
|
||||
'type' => $chineseType,
|
||||
'quantity' => $changeQty,
|
||||
'balance_before' => $currentQty,
|
||||
'balance_after' => $newQty,
|
||||
'reason' => ($validated['reason'] ?? '編輯頁面更新') . ($validated['notes'] ?? ''),
|
||||
'actual_time' => now(), // 手動調整設定為當下
|
||||
'reason' => $reason,
|
||||
'actual_time' => now(),
|
||||
'user_id' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
@@ -303,8 +397,13 @@ class InventoryController extends Controller
|
||||
{
|
||||
$inventory = \App\Models\Inventory::findOrFail($inventoryId);
|
||||
|
||||
// 歸零異動
|
||||
// 庫存 > 0 不允許刪除 (哪怕是軟刪除)
|
||||
if ($inventory->quantity > 0) {
|
||||
return redirect()->back()->with('error', '庫存數量大於 0,無法刪除。請先進行出庫或調整。');
|
||||
}
|
||||
|
||||
// 歸零異動 (因為已經限制為 0 才能刪,這段邏輯可以簡化,但為了保險起見,若有微小殘值仍可記錄歸零)
|
||||
if (abs($inventory->quantity) > 0.0001) {
|
||||
$inventory->transactions()->create([
|
||||
'type' => '手動編輯',
|
||||
'quantity' => -$inventory->quantity,
|
||||
@@ -322,33 +421,117 @@ class InventoryController extends Controller
|
||||
->with('success', '庫存品項已刪除');
|
||||
}
|
||||
|
||||
public function history(\App\Models\Warehouse $warehouse, $inventoryId)
|
||||
public function history(Request $request, \App\Models\Warehouse $warehouse)
|
||||
{
|
||||
$inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||
}, 'transactions.user'])->findOrFail($inventoryId);
|
||||
$inventoryId = $request->query('inventoryId');
|
||||
$productId = $request->query('productId');
|
||||
|
||||
$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'),
|
||||
];
|
||||
});
|
||||
if ($productId) {
|
||||
// 商品層級查詢
|
||||
$inventories = \App\Models\Inventory::where('warehouse_id', $warehouse->id)
|
||||
->where('product_id', $productId)
|
||||
->with(['product', 'transactions' => function($query) {
|
||||
$query->orderBy('actual_time', 'desc')->orderBy('id', 'desc');
|
||||
}, 'transactions.user'])
|
||||
->get();
|
||||
|
||||
return \Inertia\Inertia::render('Warehouse/InventoryHistory', [
|
||||
'warehouse' => $warehouse,
|
||||
'inventory' => [
|
||||
'id' => (string) $inventory->id,
|
||||
'productName' => $inventory->product?->name ?? '未知商品',
|
||||
'productCode' => $inventory->product?->code ?? 'N/A',
|
||||
'quantity' => (float) $inventory->quantity,
|
||||
],
|
||||
'transactions' => $transactions
|
||||
]);
|
||||
if ($inventories->isEmpty()) {
|
||||
return redirect()->back()->with('error', '找不到該商品的庫存紀錄');
|
||||
}
|
||||
|
||||
$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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// 依時間倒序排序 (最新的在前面)
|
||||
$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();
|
||||
|
||||
return \Inertia\Inertia::render('Warehouse/InventoryHistory', [
|
||||
'warehouse' => $warehouse,
|
||||
'inventory' => [
|
||||
'id' => 'product-' . $productId,
|
||||
'productName' => $productName,
|
||||
'productCode' => $productCode,
|
||||
'quantity' => (float) $currentTotalQuantity,
|
||||
],
|
||||
'transactions' => $transactions
|
||||
]);
|
||||
}
|
||||
|
||||
if ($inventoryId) {
|
||||
// 單一批號查詢
|
||||
$inventory = \App\Models\Inventory::with(['product', 'transactions' => function($query) {
|
||||
$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'),
|
||||
];
|
||||
});
|
||||
|
||||
return \Inertia\Inertia::render('Warehouse/InventoryHistory', [
|
||||
'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', '未提供查詢參數');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,11 +73,19 @@ class ProductionOrderController extends Controller
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
$status = $request->input('status', 'draft'); // 預設為草稿
|
||||
|
||||
// 共用驗證規則
|
||||
$baseRules = [
|
||||
'product_id' => 'required|exists:products,id',
|
||||
'output_batch_number' => 'required|string|max:50',
|
||||
'status' => 'nullable|in:draft,completed',
|
||||
];
|
||||
|
||||
// 完成模式需要完整驗證
|
||||
$completedRules = [
|
||||
'warehouse_id' => 'required|exists:warehouses,id',
|
||||
'output_quantity' => 'required|numeric|min:0.01',
|
||||
'output_batch_number' => 'required|string|max:50',
|
||||
'output_box_count' => 'nullable|string|max:10',
|
||||
'production_date' => 'required|date',
|
||||
'expiry_date' => 'nullable|date|after_or_equal:production_date',
|
||||
@@ -86,64 +94,96 @@ class ProductionOrderController extends Controller
|
||||
'items.*.inventory_id' => 'required|exists:inventories,id',
|
||||
'items.*.quantity_used' => 'required|numeric|min:0.0001',
|
||||
'items.*.unit_id' => 'nullable|exists:units,id',
|
||||
], [
|
||||
];
|
||||
|
||||
// 草稿模式的寬鬆規則
|
||||
$draftRules = [
|
||||
'warehouse_id' => 'nullable|exists:warehouses,id',
|
||||
'output_quantity' => 'nullable|numeric|min:0',
|
||||
'output_box_count' => 'nullable|string|max:10',
|
||||
'production_date' => 'nullable|date',
|
||||
'expiry_date' => 'nullable|date',
|
||||
'remark' => 'nullable|string',
|
||||
'items' => 'nullable|array',
|
||||
'items.*.inventory_id' => 'nullable|exists:inventories,id',
|
||||
'items.*.quantity_used' => 'nullable|numeric|min:0',
|
||||
'items.*.unit_id' => 'nullable|exists:units,id',
|
||||
];
|
||||
|
||||
$rules = $status === 'completed'
|
||||
? array_merge($baseRules, $completedRules)
|
||||
: array_merge($baseRules, $draftRules);
|
||||
|
||||
$validated = $request->validate($rules, [
|
||||
'product_id.required' => '請選擇成品商品',
|
||||
'output_batch_number.required' => '請輸入成品批號',
|
||||
'warehouse_id.required' => '請選擇入庫倉庫',
|
||||
'output_quantity.required' => '請輸入生產數量',
|
||||
'output_batch_number.required' => '請輸入成品批號',
|
||||
'production_date.required' => '請選擇生產日期',
|
||||
'items.required' => '請至少新增一項原物料',
|
||||
'items.min' => '請至少新增一項原物料',
|
||||
]);
|
||||
|
||||
DB::transaction(function () use ($validated, $request) {
|
||||
DB::transaction(function () use ($validated, $request, $status) {
|
||||
// 1. 建立生產工單
|
||||
$productionOrder = ProductionOrder::create([
|
||||
'code' => ProductionOrder::generateCode(),
|
||||
'product_id' => $validated['product_id'],
|
||||
'warehouse_id' => $validated['warehouse_id'],
|
||||
'output_quantity' => $validated['output_quantity'],
|
||||
'warehouse_id' => $validated['warehouse_id'] ?? null,
|
||||
'output_quantity' => $validated['output_quantity'] ?? 0,
|
||||
'output_batch_number' => $validated['output_batch_number'],
|
||||
'output_box_count' => $validated['output_box_count'] ?? null,
|
||||
'production_date' => $validated['production_date'],
|
||||
'production_date' => $validated['production_date'] ?? now()->toDateString(),
|
||||
'expiry_date' => $validated['expiry_date'] ?? null,
|
||||
'user_id' => auth()->id(),
|
||||
'status' => 'completed',
|
||||
'status' => $status,
|
||||
'remark' => $validated['remark'] ?? null,
|
||||
]);
|
||||
|
||||
// 2. 建立明細並扣減原物料庫存
|
||||
foreach ($validated['items'] as $item) {
|
||||
// 建立明細
|
||||
ProductionOrderItem::create([
|
||||
'production_order_id' => $productionOrder->id,
|
||||
'inventory_id' => $item['inventory_id'],
|
||||
'quantity_used' => $item['quantity_used'],
|
||||
'unit_id' => $item['unit_id'] ?? null,
|
||||
]);
|
||||
// 2. 建立明細 (草稿與完成模式皆需儲存)
|
||||
if (!empty($validated['items'])) {
|
||||
foreach ($validated['items'] as $item) {
|
||||
if (empty($item['inventory_id'])) continue;
|
||||
|
||||
// 扣減原物料庫存
|
||||
$inventory = Inventory::findOrFail($item['inventory_id']);
|
||||
$inventory->decrement('quantity', $item['quantity_used']);
|
||||
// 建立明細
|
||||
ProductionOrderItem::create([
|
||||
'production_order_id' => $productionOrder->id,
|
||||
'inventory_id' => $item['inventory_id'],
|
||||
'quantity_used' => $item['quantity_used'] ?? 0,
|
||||
'unit_id' => $item['unit_id'] ?? null,
|
||||
]);
|
||||
|
||||
// 若為完成模式,則扣減原物料庫存
|
||||
if ($status === 'completed') {
|
||||
$inventory = Inventory::findOrFail($item['inventory_id']);
|
||||
$inventory->decrement('quantity', $item['quantity_used']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 成品入庫:在目標倉庫建立新的庫存紀錄
|
||||
$product = Product::findOrFail($validated['product_id']);
|
||||
Inventory::create([
|
||||
'warehouse_id' => $validated['warehouse_id'],
|
||||
'product_id' => $validated['product_id'],
|
||||
'quantity' => $validated['output_quantity'],
|
||||
'batch_number' => $validated['output_batch_number'],
|
||||
'box_number' => $validated['output_box_count'],
|
||||
'origin_country' => 'TW', // 生產預設為本地
|
||||
'arrival_date' => $validated['production_date'],
|
||||
'expiry_date' => $validated['expiry_date'] ?? null,
|
||||
'quality_status' => 'normal',
|
||||
]);
|
||||
// 3. 若為完成模式,執行成品入庫
|
||||
if ($status === 'completed') {
|
||||
$product = Product::findOrFail($validated['product_id']);
|
||||
Inventory::create([
|
||||
'warehouse_id' => $validated['warehouse_id'],
|
||||
'product_id' => $validated['product_id'],
|
||||
'quantity' => $validated['output_quantity'],
|
||||
'batch_number' => $validated['output_batch_number'],
|
||||
'box_number' => $validated['output_box_count'],
|
||||
'origin_country' => 'TW', // 生產預設為本地
|
||||
'arrival_date' => $validated['production_date'],
|
||||
'expiry_date' => $validated['expiry_date'] ?? null,
|
||||
'quality_status' => 'normal',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$message = $status === 'completed'
|
||||
? '生產單已建立,原物料已扣減,成品已入庫'
|
||||
: '生產單草稿已儲存';
|
||||
|
||||
return redirect()->route('production-orders.index')
|
||||
->with('success', '生產單已建立,原物料已扣減,成品已入庫');
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +210,7 @@ class ProductionOrderController extends Controller
|
||||
*/
|
||||
public function getWarehouseInventories(Warehouse $warehouse)
|
||||
{
|
||||
$inventories = Inventory::with(['product.baseUnit'])
|
||||
$inventories = Inventory::with(['product.baseUnit', 'product.largeUnit'])
|
||||
->where('warehouse_id', $warehouse->id)
|
||||
->where('quantity', '>', 0)
|
||||
->where('quality_status', 'normal')
|
||||
@@ -188,9 +228,158 @@ class ProductionOrderController extends Controller
|
||||
'arrival_date' => $inv->arrival_date?->format('Y-m-d'),
|
||||
'expiry_date' => $inv->expiry_date?->format('Y-m-d'),
|
||||
'unit_name' => $inv->product->baseUnit?->name,
|
||||
'base_unit_id' => $inv->product->base_unit_id,
|
||||
'base_unit_name' => $inv->product->baseUnit?->name,
|
||||
'large_unit_id' => $inv->product->large_unit_id,
|
||||
'large_unit_name' => $inv->product->largeUnit?->name,
|
||||
'conversion_rate' => $inv->product->conversion_rate,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($inventories);
|
||||
}
|
||||
|
||||
/**
|
||||
* 編輯生產單(僅限草稿狀態)
|
||||
*/
|
||||
public function edit(ProductionOrder $productionOrder): Response
|
||||
{
|
||||
// 只有草稿可以編輯
|
||||
if ($productionOrder->status !== 'draft') {
|
||||
return redirect()->route('production-orders.show', $productionOrder->id)
|
||||
->with('error', '只有草稿狀態的生產單可以編輯');
|
||||
}
|
||||
|
||||
$productionOrder->load(['product', 'warehouse', 'items.inventory.product', 'items.unit']);
|
||||
|
||||
return Inertia::render('Production/Edit', [
|
||||
'productionOrder' => $productionOrder,
|
||||
'products' => Product::with(['baseUnit'])->get(),
|
||||
'warehouses' => Warehouse::all(),
|
||||
'units' => Unit::all(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新生產單
|
||||
*/
|
||||
public function update(Request $request, ProductionOrder $productionOrder)
|
||||
{
|
||||
// 只有草稿可以編輯
|
||||
if ($productionOrder->status !== 'draft') {
|
||||
return redirect()->route('production-orders.show', $productionOrder->id)
|
||||
->with('error', '只有草稿狀態的生產單可以編輯');
|
||||
}
|
||||
|
||||
$status = $request->input('status', 'draft');
|
||||
|
||||
// 共用驗證規則
|
||||
$baseRules = [
|
||||
'product_id' => 'required|exists:products,id',
|
||||
'output_batch_number' => 'required|string|max:50',
|
||||
'status' => 'nullable|in:draft,completed',
|
||||
];
|
||||
|
||||
// 完成模式需要完整驗證
|
||||
$completedRules = [
|
||||
'warehouse_id' => 'required|exists:warehouses,id',
|
||||
'output_quantity' => 'required|numeric|min:0.01',
|
||||
'output_box_count' => 'nullable|string|max:10',
|
||||
'production_date' => 'required|date',
|
||||
'expiry_date' => 'nullable|date|after_or_equal:production_date',
|
||||
'remark' => 'nullable|string',
|
||||
'items' => 'required|array|min:1',
|
||||
'items.*.inventory_id' => 'required|exists:inventories,id',
|
||||
'items.*.quantity_used' => 'required|numeric|min:0.0001',
|
||||
'items.*.unit_id' => 'nullable|exists:units,id',
|
||||
];
|
||||
|
||||
// 草稿模式的寬鬆規則
|
||||
$draftRules = [
|
||||
'warehouse_id' => 'nullable|exists:warehouses,id',
|
||||
'output_quantity' => 'nullable|numeric|min:0',
|
||||
'output_box_count' => 'nullable|string|max:10',
|
||||
'production_date' => 'nullable|date',
|
||||
'expiry_date' => 'nullable|date',
|
||||
'remark' => 'nullable|string',
|
||||
'items' => 'nullable|array',
|
||||
'items.*.inventory_id' => 'nullable|exists:inventories,id',
|
||||
'items.*.quantity_used' => 'nullable|numeric|min:0',
|
||||
'items.*.unit_id' => 'nullable|exists:units,id',
|
||||
];
|
||||
|
||||
$rules = $status === 'completed'
|
||||
? array_merge($baseRules, $completedRules)
|
||||
: array_merge($baseRules, $draftRules);
|
||||
|
||||
$validated = $request->validate($rules, [
|
||||
'product_id.required' => '請選擇成品商品',
|
||||
'output_batch_number.required' => '請輸入成品批號',
|
||||
'warehouse_id.required' => '請選擇入庫倉庫',
|
||||
'output_quantity.required' => '請輸入生產數量',
|
||||
'production_date.required' => '請選擇生產日期',
|
||||
'items.required' => '請至少新增一項原物料',
|
||||
'items.min' => '請至少新增一項原物料',
|
||||
]);
|
||||
|
||||
DB::transaction(function () use ($validated, $status, $productionOrder) {
|
||||
// 更新生產工單基本資料
|
||||
$productionOrder->update([
|
||||
'product_id' => $validated['product_id'],
|
||||
'warehouse_id' => $validated['warehouse_id'] ?? null,
|
||||
'output_quantity' => $validated['output_quantity'] ?? 0,
|
||||
'output_batch_number' => $validated['output_batch_number'],
|
||||
'output_box_count' => $validated['output_box_count'] ?? null,
|
||||
'production_date' => $validated['production_date'] ?? now()->toDateString(),
|
||||
'expiry_date' => $validated['expiry_date'] ?? null,
|
||||
'status' => $status,
|
||||
'remark' => $validated['remark'] ?? null,
|
||||
]);
|
||||
|
||||
// 刪除舊的明細
|
||||
$productionOrder->items()->delete();
|
||||
|
||||
// 重新建立明細 (草稿與完成模式皆需儲存)
|
||||
if (!empty($validated['items'])) {
|
||||
foreach ($validated['items'] as $item) {
|
||||
if (empty($item['inventory_id'])) continue;
|
||||
|
||||
ProductionOrderItem::create([
|
||||
'production_order_id' => $productionOrder->id,
|
||||
'inventory_id' => $item['inventory_id'],
|
||||
'quantity_used' => $item['quantity_used'] ?? 0,
|
||||
'unit_id' => $item['unit_id'] ?? null,
|
||||
]);
|
||||
|
||||
// 若為完成模式,則扣減原物料庫存
|
||||
if ($status === 'completed') {
|
||||
$inventory = Inventory::findOrFail($item['inventory_id']);
|
||||
$inventory->decrement('quantity', $item['quantity_used']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 若為完成模式,執行成品入庫
|
||||
if ($status === 'completed') {
|
||||
Inventory::create([
|
||||
'warehouse_id' => $validated['warehouse_id'],
|
||||
'product_id' => $validated['product_id'],
|
||||
'quantity' => $validated['output_quantity'],
|
||||
'batch_number' => $validated['output_batch_number'],
|
||||
'box_number' => $validated['output_box_count'],
|
||||
'origin_country' => 'TW',
|
||||
'arrival_date' => $validated['production_date'],
|
||||
'expiry_date' => $validated['expiry_date'] ?? null,
|
||||
'quality_status' => 'normal',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$message = $status === 'completed'
|
||||
? '生產單已完成,原物料已扣減,成品已入庫'
|
||||
: '生產單草稿已更新';
|
||||
|
||||
return redirect()->route('production-orders.index')
|
||||
->with('success', $message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Warehouse;
|
||||
use App\Models\Inventory;
|
||||
use App\Models\WarehouseProductSafetyStock;
|
||||
use App\Models\Product;
|
||||
use App\Models\Inventory;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -16,8 +17,6 @@ class SafetyStockController extends Controller
|
||||
*/
|
||||
public function index(Warehouse $warehouse)
|
||||
{
|
||||
$warehouse->load(['inventories.product.category']);
|
||||
|
||||
$allProducts = Product::with(['category', 'baseUnit'])->get();
|
||||
|
||||
// 準備可選商品列表
|
||||
@@ -30,32 +29,34 @@ class SafetyStockController extends Controller
|
||||
];
|
||||
});
|
||||
|
||||
// 準備現有庫存列表 (用於狀態計算)
|
||||
$inventories = $warehouse->inventories->map(function ($inv) {
|
||||
return [
|
||||
'id' => (string) $inv->id,
|
||||
'productId' => (string) $inv->product_id,
|
||||
'quantity' => (float) $inv->quantity,
|
||||
'safetyStock' => (float) $inv->safety_stock,
|
||||
];
|
||||
});
|
||||
|
||||
// 準備安全庫存設定列表
|
||||
$safetyStockSettings = $warehouse->inventories->filter(function($inv) {
|
||||
return !is_null($inv->safety_stock);
|
||||
})->map(function ($inv) {
|
||||
return [
|
||||
'id' => (string) $inv->id,
|
||||
'warehouseId' => (string) $inv->warehouse_id,
|
||||
'productId' => (string) $inv->product_id,
|
||||
'productName' => $inv->product->name,
|
||||
'productType' => $inv->product->category ? $inv->product->category->name : '其他',
|
||||
'safetyStock' => (float) $inv->safety_stock,
|
||||
'unit' => $inv->product->baseUnit?->name ?? '個',
|
||||
'updatedAt' => $inv->updated_at->toIso8601String(),
|
||||
];
|
||||
})->values();
|
||||
// 準備現有庫存列表 (用於庫存量對比)
|
||||
$inventories = Inventory::where('warehouse_id', $warehouse->id)
|
||||
->select('product_id', DB::raw('SUM(quantity) as total_quantity'))
|
||||
->groupBy('product_id')
|
||||
->get()
|
||||
->map(function ($inv) {
|
||||
return [
|
||||
'productId' => (string) $inv->product_id,
|
||||
'quantity' => (float) $inv->total_quantity,
|
||||
];
|
||||
});
|
||||
|
||||
// 準備安全庫存設定列表 (從新表格讀取)
|
||||
$safetyStockSettings = WarehouseProductSafetyStock::where('warehouse_id', $warehouse->id)
|
||||
->with(['product.category', 'product.baseUnit'])
|
||||
->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 ? $setting->product->category->name : '其他',
|
||||
'safetyStock' => (float) $setting->safety_stock,
|
||||
'unit' => $setting->product->baseUnit?->name ?? '個',
|
||||
'updatedAt' => $setting->updated_at->toIso8601String(),
|
||||
];
|
||||
});
|
||||
|
||||
return Inertia::render('Warehouse/SafetyStockSettings', [
|
||||
'warehouse' => $warehouse,
|
||||
@@ -78,7 +79,7 @@ class SafetyStockController extends Controller
|
||||
|
||||
DB::transaction(function () use ($validated, $warehouse) {
|
||||
foreach ($validated['settings'] as $item) {
|
||||
Inventory::updateOrCreate(
|
||||
WarehouseProductSafetyStock::updateOrCreate(
|
||||
[
|
||||
'warehouse_id' => $warehouse->id,
|
||||
'product_id' => $item['productId'],
|
||||
@@ -96,13 +97,13 @@ class SafetyStockController extends Controller
|
||||
/**
|
||||
* 更新單筆安全庫存設定
|
||||
*/
|
||||
public function update(Request $request, Warehouse $warehouse, Inventory $inventory)
|
||||
public function update(Request $request, Warehouse $warehouse, WarehouseProductSafetyStock $safetyStock)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'safetyStock' => 'required|numeric|min:0',
|
||||
]);
|
||||
|
||||
$inventory->update([
|
||||
$safetyStock->update([
|
||||
'safety_stock' => $validated['safetyStock'],
|
||||
]);
|
||||
|
||||
@@ -110,13 +111,11 @@ class SafetyStockController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* 刪除 (歸零) 安全庫存設定
|
||||
* 刪除安全庫存設定
|
||||
*/
|
||||
public function destroy(Warehouse $warehouse, Inventory $inventory)
|
||||
public function destroy(Warehouse $warehouse, WarehouseProductSafetyStock $safetyStock)
|
||||
{
|
||||
$inventory->update([
|
||||
'safety_stock' => null,
|
||||
]);
|
||||
$safetyStock->delete();
|
||||
|
||||
return redirect()->back()->with('success', '安全庫存設定已移除');
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ class TransferOrderController extends Controller
|
||||
],
|
||||
[
|
||||
'quantity' => 0,
|
||||
'safety_stock' => null, // 預設為 null (未設定),而非 0
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -23,9 +23,6 @@ class WarehouseController extends Controller
|
||||
}
|
||||
|
||||
$warehouses = $query->withSum('inventories as total_quantity', 'quantity')
|
||||
->withCount(['inventories as low_stock_count' => function ($query) {
|
||||
$query->whereColumn('quantity', '<', 'safety_stock');
|
||||
}])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
@@ -9,13 +9,13 @@ class Inventory extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\InventoryFactory> */
|
||||
use HasFactory;
|
||||
use \Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use \Spatie\Activitylog\Traits\LogsActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'warehouse_id',
|
||||
'product_id',
|
||||
'quantity',
|
||||
'safety_stock',
|
||||
'location',
|
||||
// 批號追溯欄位
|
||||
'batch_number',
|
||||
@@ -121,7 +121,9 @@ class Inventory extends Model
|
||||
$dateFormatted = date('Ymd', strtotime($arrivalDate));
|
||||
$prefix = "{$productCode}-{$originCountry}-{$dateFormatted}-";
|
||||
|
||||
$lastBatch = static::where('batch_number', 'like', "{$prefix}%")
|
||||
// 加入 withTrashed() 確保流水號不會撞到已刪除的紀錄
|
||||
$lastBatch = static::withTrashed()
|
||||
->where('batch_number', 'like', "{$prefix}%")
|
||||
->orderByDesc('batch_number')
|
||||
->first();
|
||||
|
||||
|
||||
41
app/Models/WarehouseProductSafetyStock.php
Normal file
41
app/Models/WarehouseProductSafetyStock.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* 倉庫-商品安全庫存設定
|
||||
* 每個倉庫-商品組合只有一筆安全庫存設定
|
||||
*/
|
||||
class WarehouseProductSafetyStock extends Model
|
||||
{
|
||||
protected $table = 'warehouse_product_safety_stocks';
|
||||
|
||||
protected $fillable = [
|
||||
'warehouse_id',
|
||||
'product_id',
|
||||
'safety_stock',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'safety_stock' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
* 所屬倉庫
|
||||
*/
|
||||
public function warehouse(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Warehouse::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 所屬商品
|
||||
*/
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user