[FEAT] 重構商品與機台報表統計邏輯,對齊實際出貨成功件數

This commit is contained in:
sky121113 2026-06-05 17:34:55 +08:00
parent 66416b14ed
commit 6173851920
2 changed files with 220 additions and 69 deletions

View File

@ -100,55 +100,113 @@ class AnalysisController extends Controller
]; ];
// 6. 執行主報表統計 SQL (按機台分組) // 6. 執行主報表統計 SQL (按機台分組)
$mainQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') // 子查詢 A營業額 (計算機台實收)
->join('orders as o', 'o.id', '=', 'oi.order_id') $salesQuery = \Illuminate\Support\Facades\DB::table('orders as o')
->leftJoin('machines as m', 'm.id', '=', 'o.machine_id')
->leftJoin('products as p', 'p.id', '=', 'oi.product_id')
->select([ ->select([
'o.machine_id', 'o.machine_id',
\Illuminate\Support\Facades\DB::raw('COALESCE(m.serial_no, "UNKNOWN") as serial_no'), \Illuminate\Support\Facades\DB::raw('SUM(o.pay_amount) as total_sales')
\Illuminate\Support\Facades\DB::raw('COALESCE(m.name, "未知機台") as name'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.quantity) as total_quantity'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) as total_sales'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.quantity * COALESCE(p.cost, 0)) as total_cost'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) - SUM(oi.quantity * COALESCE(p.cost, 0)) as total_profit')
]) ])
->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS)
->whereNull('o.deleted_at') ->whereNull('o.deleted_at')
->whereBetween('o.created_at', [$startDate, $endDate]); ->whereBetween('o.created_at', [$startDate, $endDate]);
// 套用公司篩選 // 套用篩選條件到營業額子查詢
if ($effectiveCompanyId) { if ($effectiveCompanyId) {
$mainQuery->where('o.company_id', $effectiveCompanyId); $salesQuery->where('o.company_id', $effectiveCompanyId);
}
if ($allowedMachineIds !== null) {
if (empty($allowedMachineIds)) {
$salesQuery->whereRaw('1 = 0');
} else {
$salesQuery->whereIn('o.machine_id', $allowedMachineIds);
}
}
if ($selectedMachineId && $selectedMachineId !== 'ALL') {
$salesQuery->where('o.machine_id', $selectedMachineId);
}
if (!empty($paymentTypes)) {
$salesQuery->whereIn('o.payment_type', $paymentTypes);
} }
// 套用機台權限限制 // 如果篩選特定商品,營業額只計算有成功出貨的該商品金額
if ($selectedProductId && $selectedProductId !== 'ALL') {
$salesQuery->join('order_items as oi', 'oi.order_id', '=', 'o.id')
->leftJoin('dispense_records as dr', function($join) {
$join->on('dr.order_id', '=', 'oi.order_id')
->on('dr.product_id', '=', 'oi.product_id')
->where('dr.dispense_status', '=', 1);
})
->where('oi.product_id', $selectedProductId)
->select([
'o.machine_id',
\Illuminate\Support\Facades\DB::raw('SUM(CASE WHEN dr.id IS NOT NULL THEN oi.subtotal ELSE 0 END) as total_sales')
]);
}
$salesQuery->groupBy('o.machine_id');
// 子查詢 B實際出貨件數與成本 (僅統計出貨成功且在範圍內)
$dispenseQuery = \Illuminate\Support\Facades\DB::table('dispense_records as dr')
->join('orders as o', 'o.id', '=', 'dr.order_id')
->leftJoin('products as p', 'p.id', '=', 'dr.product_id')
->select([
'dr.machine_id',
\Illuminate\Support\Facades\DB::raw('COUNT(dr.id) as total_quantity'),
\Illuminate\Support\Facades\DB::raw('SUM(COALESCE(p.cost, 0)) as total_cost')
])
->where('dr.dispense_status', 1)
->whereNull('o.deleted_at')
->whereBetween('o.created_at', [$startDate, $endDate]);
// 套用篩選條件到出貨子查詢
if ($effectiveCompanyId) {
$dispenseQuery->where('o.company_id', $effectiveCompanyId);
}
if ($allowedMachineIds !== null) {
if (empty($allowedMachineIds)) {
$dispenseQuery->whereRaw('1 = 0');
} else {
$dispenseQuery->whereIn('o.machine_id', $allowedMachineIds);
}
}
if ($selectedMachineId && $selectedMachineId !== 'ALL') {
$dispenseQuery->where('o.machine_id', $selectedMachineId);
}
if ($selectedProductId && $selectedProductId !== 'ALL') {
$dispenseQuery->where('dr.product_id', $selectedProductId);
}
if (!empty($paymentTypes)) {
$dispenseQuery->whereIn('o.payment_type', $paymentTypes);
}
$dispenseQuery->groupBy('dr.machine_id');
// 聯結機台與子查詢,取得最終統計資料
$mainQuery = \Illuminate\Support\Facades\DB::table('machines as m')
->joinSub($salesQuery, 'sq', 'sq.machine_id', '=', 'm.id')
->leftJoinSub($dispenseQuery, 'dq', 'dq.machine_id', '=', 'm.id')
->select([
'm.id as machine_id',
\Illuminate\Support\Facades\DB::raw('COALESCE(m.serial_no, "UNKNOWN") as serial_no'),
\Illuminate\Support\Facades\DB::raw('COALESCE(m.name, "未知機台") as name'),
\Illuminate\Support\Facades\DB::raw('CAST(COALESCE(dq.total_quantity, 0) as SIGNED) as total_quantity'),
\Illuminate\Support\Facades\DB::raw('CAST(COALESCE(sq.total_sales, 0) as DECIMAL(10,2)) as total_sales'),
\Illuminate\Support\Facades\DB::raw('CAST(COALESCE(dq.total_cost, 0) as DECIMAL(10,2)) as total_cost'),
\Illuminate\Support\Facades\DB::raw('CAST((COALESCE(sq.total_sales, 0) - COALESCE(dq.total_cost, 0)) as DECIMAL(10,2)) as total_profit')
]);
if ($effectiveCompanyId) {
$mainQuery->where('m.company_id', $effectiveCompanyId);
}
if ($allowedMachineIds !== null) { if ($allowedMachineIds !== null) {
if (empty($allowedMachineIds)) { if (empty($allowedMachineIds)) {
$mainQuery->whereRaw('1 = 0'); $mainQuery->whereRaw('1 = 0');
} else { } else {
$mainQuery->whereIn('o.machine_id', $allowedMachineIds); $mainQuery->whereIn('m.id', $allowedMachineIds);
} }
} }
// 套用特定機台篩選
if ($selectedMachineId && $selectedMachineId !== 'ALL') { if ($selectedMachineId && $selectedMachineId !== 'ALL') {
$mainQuery->where('o.machine_id', $selectedMachineId); $mainQuery->where('m.id', $selectedMachineId);
} }
// 套用特定商品篩選
if ($selectedProductId && $selectedProductId !== 'ALL') {
$mainQuery->where('oi.product_id', $selectedProductId);
}
// 套用支付工具多選篩選
if (!empty($paymentTypes)) {
$mainQuery->whereIn('o.payment_type', $paymentTypes);
}
$mainQuery->groupBy('o.machine_id')
->groupBy(\Illuminate\Support\Facades\DB::raw('COALESCE(m.serial_no, "UNKNOWN")'))
->groupBy(\Illuminate\Support\Facades\DB::raw('COALESCE(m.name, "未知機台")'));
$tableData = $mainQuery->get(); $tableData = $mainQuery->get();
// 7. 計算總計 Summary // 7. 計算總計 Summary
@ -176,46 +234,101 @@ class AnalysisController extends Controller
'total_profit' => round($totalProfit, 2), 'total_profit' => round($totalProfit, 2),
]; ];
// 8. 圖表日走勢統計 SQL // 8. 圖表日走勢統計
$trendQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') // 每日營業額
->join('orders as o', 'o.id', '=', 'oi.order_id') $trendSalesQuery = \Illuminate\Support\Facades\DB::table('orders as o')
->leftJoin('products as p', 'p.id', '=', 'oi.product_id')
->select([ ->select([
\Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"), \Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"),
\Illuminate\Support\Facades\DB::raw('SUM(oi.quantity) as total_quantity'), \Illuminate\Support\Facades\DB::raw('SUM(o.pay_amount) as total_sales')
\Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) as total_sales'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.quantity * COALESCE(p.cost, 0)) as total_cost'),
]) ])
->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS)
->whereNull('o.deleted_at') ->whereNull('o.deleted_at')
->whereBetween('o.created_at', [$startDate, $endDate]); ->whereBetween('o.created_at', [$startDate, $endDate]);
// 套用篩選到走勢營業額
if ($effectiveCompanyId) { if ($effectiveCompanyId) {
$trendQuery->where('o.company_id', $effectiveCompanyId); $trendSalesQuery->where('o.company_id', $effectiveCompanyId);
} }
if ($allowedMachineIds !== null) { if ($allowedMachineIds !== null) {
if (empty($allowedMachineIds)) { if (empty($allowedMachineIds)) {
$trendQuery->whereRaw('1 = 0'); $trendSalesQuery->whereRaw('1 = 0');
} else { } else {
$trendQuery->whereIn('o.machine_id', $allowedMachineIds); $trendSalesQuery->whereIn('o.machine_id', $allowedMachineIds);
} }
} }
if ($selectedMachineId && $selectedMachineId !== 'ALL') { if ($selectedMachineId && $selectedMachineId !== 'ALL') {
$trendQuery->where('o.machine_id', $selectedMachineId); $trendSalesQuery->where('o.machine_id', $selectedMachineId);
} }
if ($selectedProductId && $selectedProductId !== 'ALL') {
$trendQuery->where('oi.product_id', $selectedProductId);
}
if (!empty($paymentTypes)) { if (!empty($paymentTypes)) {
$trendQuery->whereIn('o.payment_type', $paymentTypes); $trendSalesQuery->whereIn('o.payment_type', $paymentTypes);
} }
$trendQuery->groupBy('date')->orderBy('date', 'asc'); // 如果篩選特定商品,營業額只計算有成功出貨的商品價格
$trendData = $trendQuery->get(); if ($selectedProductId && $selectedProductId !== 'ALL') {
$trendSalesQuery->join('order_items as oi', 'oi.order_id', '=', 'o.id')
->leftJoin('dispense_records as dr', function($join) {
$join->on('dr.order_id', '=', 'oi.order_id')
->on('dr.product_id', '=', 'oi.product_id')
->where('dr.dispense_status', '=', 1);
})
->where('oi.product_id', $selectedProductId)
->select([
\Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"),
\Illuminate\Support\Facades\DB::raw('SUM(CASE WHEN dr.id IS NOT NULL THEN oi.subtotal ELSE 0 END) as total_sales')
]);
}
$trendSalesQuery->groupBy('date');
$trendSalesData = $trendSalesQuery->get();
// 每日出貨件數與成本
$trendDispenseQuery = \Illuminate\Support\Facades\DB::table('dispense_records as dr')
->join('orders as o', 'o.id', '=', 'dr.order_id')
->leftJoin('products as p', 'p.id', '=', 'dr.product_id')
->select([
\Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"),
\Illuminate\Support\Facades\DB::raw('COUNT(dr.id) as total_quantity'),
\Illuminate\Support\Facades\DB::raw('SUM(COALESCE(p.cost, 0)) as total_cost')
])
->where('dr.dispense_status', 1)
->whereNull('o.deleted_at')
->whereBetween('o.created_at', [$startDate, $endDate]);
// 套用篩選到走勢出貨
if ($effectiveCompanyId) {
$trendDispenseQuery->where('o.company_id', $effectiveCompanyId);
}
if ($allowedMachineIds !== null) {
if (empty($allowedMachineIds)) {
$trendDispenseQuery->whereRaw('1 = 0');
} else {
$trendDispenseQuery->whereIn('o.machine_id', $allowedMachineIds);
}
}
if ($selectedMachineId && $selectedMachineId !== 'ALL') {
$trendDispenseQuery->where('o.machine_id', $selectedMachineId);
}
if ($selectedProductId && $selectedProductId !== 'ALL') {
$trendDispenseQuery->where('dr.product_id', $selectedProductId);
}
if (!empty($paymentTypes)) {
$trendDispenseQuery->whereIn('o.payment_type', $paymentTypes);
}
$trendDispenseQuery->groupBy('date');
$trendDispenseData = $trendDispenseQuery->get();
// 將子查詢結果轉換為 PHP Map 對照
$salesMap = [];
foreach ($trendSalesData as $row) {
$salesMap[$row->date] = $row->total_sales;
}
$dispenseMap = [];
foreach ($trendDispenseData as $row) {
$dispenseMap[$row->date] = [
'quantity' => $row->total_quantity,
'cost' => $row->total_cost
];
}
// 補齊日期區間內所有空缺日走勢 // 補齊日期區間內所有空缺日走勢
$period = new \DatePeriod( $period = new \DatePeriod(
@ -227,21 +340,16 @@ class AnalysisController extends Controller
$chartData = []; $chartData = [];
foreach ($period as $date) { foreach ($period as $date) {
$dateStr = $date->format('Y-m-d'); $dateStr = $date->format('Y-m-d');
$hasSales = isset($salesMap[$dateStr]);
$hasDispense = isset($dispenseMap[$dateStr]);
$chartData[$dateStr] = [ $chartData[$dateStr] = [
'date' => $dateStr, 'date' => $dateStr,
'quantity' => 0, 'quantity' => $hasDispense ? (int)$dispenseMap[$dateStr]['quantity'] : 0,
'sales' => 0.0, 'sales' => $hasSales ? round((float)$salesMap[$dateStr], 2) : 0.0,
'cost' => 0.0, 'cost' => $hasDispense ? round((float)$dispenseMap[$dateStr]['cost'], 2) : 0.0,
]; ];
} }
foreach ($trendData as $row) {
if (isset($chartData[$row->date])) {
$chartData[$row->date]['quantity'] = (int)$row->total_quantity;
$chartData[$row->date]['sales'] = round((float)$row->total_sales, 2);
$chartData[$row->date]['cost'] = round((float)$row->total_cost, 2);
}
}
$chartData = array_values($chartData); $chartData = array_values($chartData);
// 9. 判斷是否為 AJAX 請求,回傳 JSON 格式 // 9. 判斷是否為 AJAX 請求,回傳 JSON 格式
@ -340,15 +448,20 @@ class AnalysisController extends Controller
// 計算各商品的銷售數量、銷售額、總成本、利潤 // 計算各商品的銷售數量、銷售額、總成本、利潤
$mainQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') $mainQuery = \Illuminate\Support\Facades\DB::table('order_items as oi')
->join('orders as o', 'o.id', '=', 'oi.order_id') ->join('orders as o', 'o.id', '=', 'oi.order_id')
->leftJoin('dispense_records as dr', function($join) {
$join->on('dr.order_id', '=', 'oi.order_id')
->on('dr.product_id', '=', 'oi.product_id')
->where('dr.dispense_status', '=', 1);
})
->leftJoin('products as p', 'p.id', '=', 'oi.product_id') ->leftJoin('products as p', 'p.id', '=', 'oi.product_id')
->select([ ->select([
'oi.product_id', 'oi.product_id',
\Illuminate\Support\Facades\DB::raw('COALESCE(p.barcode, oi.barcode) as barcode'), \Illuminate\Support\Facades\DB::raw('COALESCE(p.barcode, oi.barcode) as barcode'),
\Illuminate\Support\Facades\DB::raw('COALESCE(p.name, oi.product_name) as name'), \Illuminate\Support\Facades\DB::raw('COALESCE(p.name, oi.product_name) as name'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.quantity) as total_quantity'), \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN 1 ELSE 0 END) as SIGNED) as total_quantity'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) as total_sales'), \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN oi.price ELSE 0 END) as DECIMAL(10,2)) as total_sales'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.quantity * COALESCE(p.cost, 0)) as total_cost'), \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN COALESCE(p.cost, 0) ELSE 0 END) as DECIMAL(10,2)) as total_cost'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) - SUM(oi.quantity * COALESCE(p.cost, 0)) as total_profit') \Illuminate\Support\Facades\DB::raw('CAST((SUM(CASE WHEN dr.id IS NOT NULL THEN oi.price ELSE 0 END) - SUM(CASE WHEN dr.id IS NOT NULL THEN COALESCE(p.cost, 0) ELSE 0 END)) as DECIMAL(10,2)) as total_profit')
]) ])
->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS)
->whereNull('o.deleted_at') ->whereNull('o.deleted_at')
@ -411,12 +524,17 @@ class AnalysisController extends Controller
// 7. 圖表日走勢統計 SQL // 7. 圖表日走勢統計 SQL
$trendQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') $trendQuery = \Illuminate\Support\Facades\DB::table('order_items as oi')
->join('orders as o', 'o.id', '=', 'oi.order_id') ->join('orders as o', 'o.id', '=', 'oi.order_id')
->leftJoin('dispense_records as dr', function($join) {
$join->on('dr.order_id', '=', 'oi.order_id')
->on('dr.product_id', '=', 'oi.product_id')
->where('dr.dispense_status', '=', 1);
})
->leftJoin('products as p', 'p.id', '=', 'oi.product_id') ->leftJoin('products as p', 'p.id', '=', 'oi.product_id')
->select([ ->select([
\Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"), \Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"),
\Illuminate\Support\Facades\DB::raw('SUM(oi.quantity) as total_quantity'), \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN 1 ELSE 0 END) as SIGNED) as total_quantity'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) as total_sales'), \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN oi.price ELSE 0 END) as DECIMAL(10,2)) as total_sales'),
\Illuminate\Support\Facades\DB::raw('SUM(oi.quantity * COALESCE(p.cost, 0)) as total_cost'), \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN COALESCE(p.cost, 0) ELSE 0 END) as DECIMAL(10,2)) as total_cost')
]) ])
->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS)
->whereNull('o.deleted_at') ->whereNull('o.deleted_at')

View File

@ -8,6 +8,7 @@ use App\Models\Machine\Machine;
use App\Models\Product\Product; use App\Models\Product\Product;
use App\Models\Transaction\Order; use App\Models\Transaction\Order;
use App\Models\Transaction\OrderItem; use App\Models\Transaction\OrderItem;
use App\Models\Transaction\DispenseRecord;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@ -143,6 +144,27 @@ class AnalysisReportsTest extends TestCase
'subtotal' => 30.00, 'subtotal' => 30.00,
]); ]);
DispenseRecord::create([
'company_id' => $this->company->id,
'order_id' => $order1->id,
'machine_id' => $this->machineA->id,
'slot_no' => '1',
'product_id' => $this->productX->id,
'amount' => 15.00,
'dispense_status' => 1,
'machine_time' => now(),
]);
DispenseRecord::create([
'company_id' => $this->company->id,
'order_id' => $order1->id,
'machine_id' => $this->machineA->id,
'slot_no' => '1',
'product_id' => $this->productX->id,
'amount' => 15.00,
'dispense_status' => 1,
'machine_time' => now(),
]);
// Order 2: Machine B, Product Y, payment type 2 (E-ticket), price 25, quantity 1. (Sales: 25, Cost: 20, Profit: 5) // Order 2: Machine B, Product Y, payment type 2 (E-ticket), price 25, quantity 1. (Sales: 25, Cost: 20, Profit: 5)
$order2 = Order::create([ $order2 = Order::create([
'company_id' => $this->company->id, 'company_id' => $this->company->id,
@ -163,6 +185,17 @@ class AnalysisReportsTest extends TestCase
'price' => 25.00, 'price' => 25.00,
'subtotal' => 25.00, 'subtotal' => 25.00,
]); ]);
DispenseRecord::create([
'company_id' => $this->company->id,
'order_id' => $order2->id,
'machine_id' => $this->machineB->id,
'slot_no' => '1',
'product_id' => $this->productY->id,
'amount' => 25.00,
'dispense_status' => 1,
'machine_time' => now(),
]);
} }
/** /**