229 lines
9.9 KiB
PHP
229 lines
9.9 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace App\Modules\Inventory\Services;
|
|||
|
|
|
|||
|
|
use App\Modules\Inventory\Models\InventoryTransaction;
|
|||
|
|
use App\Modules\Inventory\Models\Product; // Use Inventory module's Product if available, or Core's? Usually Product is in Inventory/Models? No, let's check.
|
|||
|
|
// Checking Product model location... likely App\Modules\Product\Models\Product or App\Modules\Inventory\Models\Product.
|
|||
|
|
// From previous context: "products.create" permission suggests a Products module.
|
|||
|
|
// But stock query uses `products` table join.
|
|||
|
|
// Let's assume standard Laravel query builder or check existing models.
|
|||
|
|
// StockQueryController uses `InventoryService`.
|
|||
|
|
// I will use DB facade or InventoryTransaction model for aggregation.
|
|||
|
|
|
|||
|
|
use Illuminate\Support\Facades\DB;
|
|||
|
|
use Illuminate\Database\Eloquent\Builder;
|
|||
|
|
|
|||
|
|
class InventoryReportService
|
|||
|
|
{
|
|||
|
|
/**
|
|||
|
|
* 取得庫存報表資料
|
|||
|
|
*
|
|||
|
|
* @param array $filters 篩選條件
|
|||
|
|
* @param int|null $perPage 每頁筆數
|
|||
|
|
* @return \Illuminate\Pagination\LengthAwarePaginator|\Illuminate\Support\Collection
|
|||
|
|
*/
|
|||
|
|
public function getReportData(array $filters, ?int $perPage = 10)
|
|||
|
|
{
|
|||
|
|
$dateFrom = $filters['date_from'] ?? null;
|
|||
|
|
$dateTo = $filters['date_to'] ?? null;
|
|||
|
|
$warehouseId = $filters['warehouse_id'] ?? null;
|
|||
|
|
$categoryId = $filters['category_id'] ?? null;
|
|||
|
|
$search = $filters['search'] ?? null;
|
|||
|
|
|
|||
|
|
// 若無任何篩選條件,直接回傳空資料
|
|||
|
|
if (!$dateFrom && !$dateTo && !$warehouseId && !$categoryId && !$search) {
|
|||
|
|
return new \Illuminate\Pagination\LengthAwarePaginator([], 0, $perPage ?: 10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定義時間欄位轉換 (UTC -> Asia/Taipei)
|
|||
|
|
// 日期欄位:Laravel 時區已設為 Asia/Taipei,直接使用 actual_time
|
|||
|
|
$timeColumn = "inventory_transactions.actual_time";
|
|||
|
|
|
|||
|
|
// 建立查詢
|
|||
|
|
// 我們需要針對每個 品項 在選定區間內 進行彙總
|
|||
|
|
// 來源:inventory_transactions -> inventory -> product
|
|||
|
|
|
|||
|
|
$query = InventoryTransaction::query()
|
|||
|
|
->select([
|
|||
|
|
'products.code as product_code',
|
|||
|
|
'products.name as product_name',
|
|||
|
|
'categories.name as category_name',
|
|||
|
|
'products.id as product_id',
|
|||
|
|
// 進貨量:type 為 入庫, 手動入庫 (排除 調撥入庫)
|
|||
|
|
DB::raw("SUM(CASE WHEN inventory_transactions.type IN ('入庫', '手動入庫') AND inventory_transactions.quantity > 0 THEN inventory_transactions.quantity ELSE 0 END) as inbound_qty"),
|
|||
|
|
// 出貨量:type 為 出庫 (排除 調撥出庫) (取絕對值)
|
|||
|
|
DB::raw("ABS(SUM(CASE WHEN inventory_transactions.type IN ('出庫') AND inventory_transactions.quantity < 0 THEN inventory_transactions.quantity ELSE 0 END)) as outbound_qty"),
|
|||
|
|
// 調整量:type 為 庫存調整, 手動編輯
|
|||
|
|
DB::raw("SUM(CASE WHEN inventory_transactions.type IN ('庫存調整', '手動編輯') THEN inventory_transactions.quantity ELSE 0 END) as adjust_qty"),
|
|||
|
|
// 調撥淨額 (隱藏欄位,但包含在 net_change)
|
|||
|
|
// 淨變動:總和 (包含所有類型:進貨、出貨、調整、調撥)
|
|||
|
|
DB::raw("SUM(inventory_transactions.quantity) as net_change"),
|
|||
|
|
])
|
|||
|
|
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
|
|||
|
|
->join('products', 'inventories.product_id', '=', 'products.id')
|
|||
|
|
->leftJoin('categories', 'products.category_id', '=', 'categories.id');
|
|||
|
|
|
|||
|
|
// 日期篩選:資料庫儲存的是台北時間,直接用字串比對
|
|||
|
|
if ($dateFrom && $dateTo) {
|
|||
|
|
$query->whereRaw("$timeColumn >= ? AND $timeColumn <= ?", [
|
|||
|
|
$dateFrom . ' 00:00:00',
|
|||
|
|
$dateTo . ' 23:59:59'
|
|||
|
|
]);
|
|||
|
|
} elseif ($dateFrom) {
|
|||
|
|
$query->whereRaw("$timeColumn >= ?", [$dateFrom . ' 00:00:00']);
|
|||
|
|
} elseif ($dateTo) {
|
|||
|
|
$query->whereRaw("$timeColumn <= ?", [$dateTo . ' 23:59:59']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 應用篩選
|
|||
|
|
if ($warehouseId) {
|
|||
|
|
$query->where('inventories.warehouse_id', $warehouseId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($categoryId) {
|
|||
|
|
$query->where('products.category_id', $categoryId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($search) {
|
|||
|
|
$query->where(function ($q) use ($search) {
|
|||
|
|
$q->where('products.name', 'like', "%{$search}%")
|
|||
|
|
->orWhere('products.code', 'like', "%{$search}%");
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 分組與排序
|
|||
|
|
$query->groupBy([
|
|||
|
|
'products.id',
|
|||
|
|
'products.code',
|
|||
|
|
'products.name',
|
|||
|
|
'categories.name'
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
$query->orderBy('products.code', 'asc');
|
|||
|
|
|
|||
|
|
|
|||
|
|
if ($perPage) {
|
|||
|
|
return $query->paginate($perPage)->withQueryString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $query->get();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 取得報表統計數據 (不分頁,針對篩選條件的全量統計)
|
|||
|
|
*/
|
|||
|
|
public function getSummary(array $filters)
|
|||
|
|
{
|
|||
|
|
$dateFrom = $filters['date_from'] ?? null;
|
|||
|
|
$dateTo = $filters['date_to'] ?? null;
|
|||
|
|
$warehouseId = $filters['warehouse_id'] ?? null;
|
|||
|
|
$categoryId = $filters['category_id'] ?? null;
|
|||
|
|
$search = $filters['search'] ?? null;
|
|||
|
|
|
|||
|
|
// 若無任何篩選條件,直接回傳零值
|
|||
|
|
if (!$dateFrom && !$dateTo && !$warehouseId && !$categoryId && !$search) {
|
|||
|
|
return (object)[
|
|||
|
|
'total_inbound' => 0,
|
|||
|
|
'total_outbound' => 0,
|
|||
|
|
'total_adjust' => 0,
|
|||
|
|
'total_net_change' => 0,
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
// 日期欄位:Laravel 時區已設為 Asia/Taipei,直接使用 actual_time
|
|||
|
|
$timeColumn = "inventory_transactions.actual_time";
|
|||
|
|
|
|||
|
|
$query = InventoryTransaction::query()
|
|||
|
|
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
|
|||
|
|
->join('products', 'inventories.product_id', '=', 'products.id')
|
|||
|
|
->leftJoin('categories', 'products.category_id', '=', 'categories.id');
|
|||
|
|
|
|||
|
|
// 日期篩選:資料庫儲存的是台北時間,直接用字串比對
|
|||
|
|
if ($dateFrom && $dateTo) {
|
|||
|
|
$query->whereRaw("$timeColumn >= ? AND $timeColumn <= ?", [
|
|||
|
|
$dateFrom . ' 00:00:00',
|
|||
|
|
$dateTo . ' 23:59:59'
|
|||
|
|
]);
|
|||
|
|
} elseif ($dateFrom) {
|
|||
|
|
$query->whereRaw("$timeColumn >= ?", [$dateFrom . ' 00:00:00']);
|
|||
|
|
} elseif ($dateTo) {
|
|||
|
|
$query->whereRaw("$timeColumn <= ?", [$dateTo . ' 23:59:59']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($warehouseId) {
|
|||
|
|
$query->where('inventories.warehouse_id', $warehouseId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($categoryId) {
|
|||
|
|
$query->where('products.category_id', $categoryId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($search) {
|
|||
|
|
$query->where(function ($q) use ($search) {
|
|||
|
|
$q->where('products.name', 'like', "%{$search}%")
|
|||
|
|
->orWhere('products.code', 'like', "%{$search}%");
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 直接聚合所有符合條件的交易
|
|||
|
|
return $query->select([
|
|||
|
|
DB::raw("SUM(CASE WHEN inventory_transactions.type IN ('入庫', '手動入庫') AND inventory_transactions.quantity > 0 THEN inventory_transactions.quantity ELSE 0 END) as total_inbound"),
|
|||
|
|
DB::raw("ABS(SUM(CASE WHEN inventory_transactions.type IN ('出庫') AND inventory_transactions.quantity < 0 THEN inventory_transactions.quantity ELSE 0 END)) as total_outbound"),
|
|||
|
|
DB::raw("SUM(CASE WHEN inventory_transactions.type IN ('庫存調整', '手動編輯') THEN inventory_transactions.quantity ELSE 0 END) as total_adjust"),
|
|||
|
|
DB::raw("SUM(inventory_transactions.quantity) as total_net_change"),
|
|||
|
|
])->first();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 取得特定商品的庫存異動明細
|
|||
|
|
*/
|
|||
|
|
public function getProductDetails($productId, array $filters, ?int $perPage = 20)
|
|||
|
|
{
|
|||
|
|
$dateFrom = $filters['date_from'] ?? null;
|
|||
|
|
$dateTo = $filters['date_to'] ?? null;
|
|||
|
|
$warehouseId = $filters['warehouse_id'] ?? null;
|
|||
|
|
// 日期欄位:Laravel 時區已設為 Asia/Taipei,直接使用 actual_time
|
|||
|
|
$timeColumn = "inventory_transactions.actual_time";
|
|||
|
|
|
|||
|
|
$query = InventoryTransaction::query()
|
|||
|
|
->select([
|
|||
|
|
'inventory_transactions.*',
|
|||
|
|
'inventories.warehouse_id',
|
|||
|
|
'inventories.batch_number as batch_no',
|
|||
|
|
'warehouses.name as warehouse_name',
|
|||
|
|
'users.name as user_name',
|
|||
|
|
'products.code as product_code',
|
|||
|
|
'products.name as product_name',
|
|||
|
|
'units.name as unit_name'
|
|||
|
|
])
|
|||
|
|
->join('inventories', 'inventory_transactions.inventory_id', '=', 'inventories.id')
|
|||
|
|
->join('products', 'inventories.product_id', '=', 'products.id')
|
|||
|
|
->leftJoin('units', 'products.base_unit_id', '=', 'units.id')
|
|||
|
|
->leftJoin('warehouses', 'inventories.warehouse_id', '=', 'warehouses.id')
|
|||
|
|
->leftJoin('users', 'inventory_transactions.user_id', '=', 'users.id')
|
|||
|
|
->where('products.id', $productId);
|
|||
|
|
|
|||
|
|
// 日期篩選:資料庫儲存的是台北時間,直接用字串比對
|
|||
|
|
if ($dateFrom && $dateTo) {
|
|||
|
|
$query->whereRaw("$timeColumn >= ? AND $timeColumn <= ?", [
|
|||
|
|
$dateFrom . ' 00:00:00',
|
|||
|
|
$dateTo . ' 23:59:59'
|
|||
|
|
]);
|
|||
|
|
} elseif ($dateFrom) {
|
|||
|
|
$query->whereRaw("$timeColumn >= ?", [$dateFrom . ' 00:00:00']);
|
|||
|
|
} elseif ($dateTo) {
|
|||
|
|
$query->whereRaw("$timeColumn <= ?", [$dateTo . ' 23:59:59']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($warehouseId) {
|
|||
|
|
$query->where('inventories.warehouse_id', $warehouseId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 排序:最新的在最上面
|
|||
|
|
$query->orderBy('inventory_transactions.actual_time', 'desc')
|
|||
|
|
->orderBy('inventory_transactions.id', 'desc');
|
|||
|
|
|
|||
|
|
return $query->paginate($perPage)->withQueryString();
|
|||
|
|
}
|
|||
|
|
}
|