refactor: 調整統計基準為明細筆數,並恢復庫存查詢為單一細目顯示模式
This commit is contained in:
@@ -246,37 +246,8 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
$join->on('inventories.warehouse_id', '=', 'ss.warehouse_id')
|
$join->on('inventories.warehouse_id', '=', 'ss.warehouse_id')
|
||||||
->on('inventories.product_id', '=', 'ss.product_id');
|
->on('inventories.product_id', '=', 'ss.product_id');
|
||||||
})
|
})
|
||||||
->whereNull('inventories.deleted_at');
|
->whereNull('inventories.deleted_at')
|
||||||
|
->select([
|
||||||
// 決定是否啟用聚合顯示 (Group By Warehouse + Product)
|
|
||||||
// 當使用者在儀表板點選「狀態篩選」時,為了讓清單筆數與統計數字精確一致 (Dimension Alignment),必須啟用 GROUP BY
|
|
||||||
$isGrouped = !empty($filters['status']) || ($filters['view_mode'] ?? '') === 'product';
|
|
||||||
|
|
||||||
if ($isGrouped) {
|
|
||||||
$query->select([
|
|
||||||
DB::raw('MIN(inventories.id) as id'), // 聚合後的代表 ID
|
|
||||||
'inventories.warehouse_id',
|
|
||||||
'inventories.product_id',
|
|
||||||
DB::raw('SUM(inventories.quantity) as quantity'),
|
|
||||||
DB::raw('GROUP_CONCAT(DISTINCT inventories.batch_number SEPARATOR ", ") as batch_number'),
|
|
||||||
DB::raw('MAX(inventories.expiry_date) as expiry_date'),
|
|
||||||
DB::raw('GROUP_CONCAT(DISTINCT inventories.location SEPARATOR ", ") as location'),
|
|
||||||
'products.code as product_code',
|
|
||||||
'products.name as product_name',
|
|
||||||
'categories.name as category_name',
|
|
||||||
'warehouses.name as warehouse_name',
|
|
||||||
'ss.safety_stock',
|
|
||||||
])->groupBy(
|
|
||||||
'inventories.warehouse_id',
|
|
||||||
'inventories.product_id',
|
|
||||||
'products.code',
|
|
||||||
'products.name',
|
|
||||||
'categories.name',
|
|
||||||
'warehouses.name',
|
|
||||||
'ss.safety_stock'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$query->select([
|
|
||||||
'inventories.id',
|
'inventories.id',
|
||||||
'inventories.warehouse_id',
|
'inventories.warehouse_id',
|
||||||
'inventories.product_id',
|
'inventories.product_id',
|
||||||
@@ -291,7 +262,6 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
'warehouses.name as warehouse_name',
|
'warehouses.name as warehouse_name',
|
||||||
'ss.safety_stock',
|
'ss.safety_stock',
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
// 篩選:倉庫
|
// 篩選:倉庫
|
||||||
if (!empty($filters['warehouse_id'])) {
|
if (!empty($filters['warehouse_id'])) {
|
||||||
@@ -381,46 +351,52 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
$query->orderBy('products.code', 'asc');
|
$query->orderBy('products.code', 'asc');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 統計卡片(無篩選條件下的全域統計)
|
// 統計卡片(預設無篩選條件下的全域統計,改為明細筆數計數以對齊顯示)
|
||||||
// 1. 庫存品項數:在庫的「倉庫-商品」對總數
|
// 1. 庫存明細總數
|
||||||
$totalItems = DB::table('inventories')
|
$totalItems = DB::table('inventories')
|
||||||
->whereNull('deleted_at')
|
->whereNull('deleted_at')
|
||||||
->distinct()
|
->count();
|
||||||
->count(DB::raw('CONCAT(warehouse_id, "-", product_id)'));
|
|
||||||
|
|
||||||
// 2. 低庫存:以「倉庫-商品」聚合後的總量與安全庫存比較 (品項計數)
|
// 2. 低庫存明細數:只要該明細所屬的「倉庫+商品」總量低於安全庫存,則所有相關明細都計入
|
||||||
$lowStockCount = DB::table('warehouse_product_safety_stocks as ss')
|
$lowStockCount = DB::table('inventories as i')
|
||||||
->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'),
|
->join('warehouse_product_safety_stocks as ss', function ($join) {
|
||||||
function ($join) {
|
$join->on('i.warehouse_id', '=', 'ss.warehouse_id')
|
||||||
$join->on('ss.warehouse_id', '=', 'inv.warehouse_id')
|
->on('i.product_id', '=', 'ss.product_id');
|
||||||
->on('ss.product_id', '=', 'inv.product_id');
|
})
|
||||||
|
->whereNull('i.deleted_at')
|
||||||
|
->whereIn(DB::raw('(i.warehouse_id, i.product_id)'), function ($sub) {
|
||||||
|
$sub->select('i2.warehouse_id', 'i2.product_id')
|
||||||
|
->from('inventories as i2')
|
||||||
|
->whereNull('i2.deleted_at')
|
||||||
|
->groupBy('i2.warehouse_id', 'i2.product_id')
|
||||||
|
->havingRaw('SUM(i2.quantity) <= (SELECT safety_stock FROM warehouse_product_safety_stocks WHERE warehouse_id = i2.warehouse_id AND product_id = i2.product_id LIMIT 1)');
|
||||||
})
|
})
|
||||||
->whereRaw('inv.total_qty <= ss.safety_stock')
|
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 3. 負庫存:只要該「倉庫-商品」的總庫存為負數 (品項計數)
|
// 3. 負庫存明細數
|
||||||
$negativeCount = DB::table(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'))
|
$negativeCount = DB::table('inventories as i')
|
||||||
->where('total_qty', '<', 0)
|
->whereNull('i.deleted_at')
|
||||||
|
->whereIn(DB::raw('(i.warehouse_id, i.product_id)'), function ($sub) {
|
||||||
|
$sub->select('i2.warehouse_id', 'i2.product_id')
|
||||||
|
->from('inventories as i2')
|
||||||
|
->whereNull('i2.deleted_at')
|
||||||
|
->groupBy('i2.warehouse_id', 'i2.product_id')
|
||||||
|
->havingRaw('SUM(i2.quantity) < 0');
|
||||||
|
})
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
// 4. 即將過期:有任一批次效期符合的「品項」總數
|
// 4. 即將過期明細數
|
||||||
$expiringCount = DB::table('inventories')
|
$expiringCount = DB::table('inventories')
|
||||||
->whereNull('deleted_at')
|
->whereNull('deleted_at')
|
||||||
->whereNotNull('expiry_date')
|
->whereNotNull('expiry_date')
|
||||||
->where('expiry_date', '<=', $expiryThreshold)
|
->where('expiry_date', '<=', $expiryThreshold)
|
||||||
->distinct()
|
->count();
|
||||||
->count(DB::raw('CONCAT(warehouse_id, "-", product_id)'));
|
|
||||||
|
|
||||||
// 分頁
|
// 分頁
|
||||||
$paginated = $query->paginate($perPage)->withQueryString();
|
$paginated = $query->paginate($perPage)->withQueryString();
|
||||||
|
|
||||||
// 為每筆紀錄附加最後入庫/出庫時間 + 狀態
|
// 為每筆紀錄附加最後入庫/出庫時間 + 狀態
|
||||||
$items = collect($paginated->items())->map(function ($item) use ($today, $expiryThreshold, $isGrouped) {
|
$items = collect($paginated->items())->map(function ($item) use ($today, $expiryThreshold) {
|
||||||
// 如果是聚合模式,Transaction 查詢也需要調整或略過單一批次查詢
|
|
||||||
$lastIn = null;
|
|
||||||
$lastOut = null;
|
|
||||||
|
|
||||||
if (!$isGrouped) {
|
|
||||||
$lastIn = \App\Modules\Inventory\Models\InventoryTransaction::where('inventory_id', $item->id)
|
$lastIn = \App\Modules\Inventory\Models\InventoryTransaction::where('inventory_id', $item->id)
|
||||||
->where('type', '入庫')
|
->where('type', '入庫')
|
||||||
->orderByDesc('actual_time')
|
->orderByDesc('actual_time')
|
||||||
@@ -430,7 +406,6 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
->where('type', '出庫')
|
->where('type', '出庫')
|
||||||
->orderByDesc('actual_time')
|
->orderByDesc('actual_time')
|
||||||
->value('actual_time');
|
->value('actual_time');
|
||||||
}
|
|
||||||
|
|
||||||
// 計算狀態
|
// 計算狀態
|
||||||
$statuses = [];
|
$statuses = [];
|
||||||
@@ -466,7 +441,6 @@ class InventoryService implements InventoryServiceInterface
|
|||||||
'last_inbound' => $lastIn ? \Carbon\Carbon::parse($lastIn)->toDateString() : null,
|
'last_inbound' => $lastIn ? \Carbon\Carbon::parse($lastIn)->toDateString() : null,
|
||||||
'last_outbound' => $lastOut ? \Carbon\Carbon::parse($lastOut)->toDateString() : null,
|
'last_outbound' => $lastOut ? \Carbon\Carbon::parse($lastOut)->toDateString() : null,
|
||||||
'statuses' => $statuses,
|
'statuses' => $statuses,
|
||||||
'is_grouped' => $isGrouped,
|
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ interface InventoryItem {
|
|||||||
last_inbound: string | null;
|
last_inbound: string | null;
|
||||||
last_outbound: string | null;
|
last_outbound: string | null;
|
||||||
statuses: string[];
|
statuses: string[];
|
||||||
is_grouped?: boolean;
|
|
||||||
location: string | null;
|
location: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,10 +492,10 @@ export default function StockQueryIndex({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{item.warehouse_name}
|
{item.warehouse_name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-gray-500 text-sm italic">
|
<TableCell className="text-gray-500 text-sm">
|
||||||
{item.batch_number || "—"}
|
{item.batch_number || "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-sm text-gray-500 italic">
|
<TableCell className="text-sm text-gray-500">
|
||||||
{item.location || "—"}
|
{item.location || "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={`text-right font-medium ${item.quantity < 0 ? "text-red-600" : ""}`}>
|
<TableCell className={`text-right font-medium ${item.quantity < 0 ? "text-red-600" : ""}`}>
|
||||||
|
|||||||
Reference in New Issue
Block a user