2026-02-10 16:07:31 +08:00
< ? 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 ;
2026-02-10 17:18:59 +08:00
$sortBy = $filters [ 'sort_by' ] ? ? 'product_code' ;
$sortOrder = $filters [ 'sort_order' ] ? ? 'asc' ;
2026-02-10 16:07:31 +08:00
// 若無任何篩選條件,直接回傳空資料
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 " ),
2026-02-10 17:18:59 +08:00
// 調撥入: type 為 調撥入庫
DB :: raw ( " SUM(CASE WHEN inventory_transactions.type = '調撥入庫' AND inventory_transactions.quantity > 0 THEN inventory_transactions.quantity ELSE 0 END) as transfer_in_qty " ),
// 調撥出: type 為 調撥出庫 (取絕對值)
DB :: raw ( " ABS(SUM(CASE WHEN inventory_transactions.type = '調撥出庫' AND inventory_transactions.quantity < 0 THEN inventory_transactions.quantity ELSE 0 END)) as transfer_out_qty " ),
2026-02-10 16:07:31 +08:00
// 調整量: type 為 庫存調整, 手動編輯
DB :: raw ( " SUM(CASE WHEN inventory_transactions.type IN ('庫存調整', '手動編輯') THEN inventory_transactions.quantity ELSE 0 END) as adjust_qty " ),
// 淨變動:總和 (包含所有類型:進貨、出貨、調整、調撥)
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 } % " );
});
}
2026-02-10 17:18:59 +08:00
// 分組
2026-02-10 16:07:31 +08:00
$query -> groupBy ([
'products.id' ,
'products.code' ,
'products.name' ,
'categories.name'
]);
2026-02-10 17:18:59 +08:00
// 動態排序
$allowedSortFields = [
'product_code' => 'products.code' ,
'product_name' => 'products.name' ,
'inbound_qty' => 'inbound_qty' ,
'outbound_qty' => 'outbound_qty' ,
'transfer_in_qty' => 'transfer_in_qty' ,
'transfer_out_qty' => 'transfer_out_qty' ,
'adjust_qty' => 'adjust_qty' ,
'net_change' => 'net_change' ,
];
$sortColumn = $allowedSortFields [ $sortBy ] ? ? 'products.code' ;
$query -> orderBy ( $sortColumn , $sortOrder === 'desc' ? 'desc' : 'asc' );
2026-02-10 16:07:31 +08:00
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 " ),
2026-02-10 17:18:59 +08:00
DB :: raw ( " SUM(CASE WHEN inventory_transactions.type = '調撥入庫' AND inventory_transactions.quantity > 0 THEN inventory_transactions.quantity ELSE 0 END) as total_transfer_in " ),
DB :: raw ( " ABS(SUM(CASE WHEN inventory_transactions.type = '調撥出庫' AND inventory_transactions.quantity < 0 THEN inventory_transactions.quantity ELSE 0 END)) as total_transfer_out " ),
2026-02-10 16:07:31 +08:00
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 ();
}
}