Files
star-erp/app/Modules/Inventory/Services/InventoryReportService.php
sky121113 220478641d
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m3s
feat: 更新庫存報表、銷售匯入及採購單相關功能
2026-02-10 17:18:59 +08:00

249 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
$sortBy = $filters['sort_by'] ?? 'product_code';
$sortOrder = $filters['sort_order'] ?? 'asc';
// 若無任何篩選條件,直接回傳空資料
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 = '調撥入庫' 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"),
// 調整量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}%");
});
}
// 分組
$query->groupBy([
'products.id',
'products.code',
'products.name',
'categories.name'
]);
// 動態排序
$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');
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 = '調撥入庫' 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"),
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();
}
}