where('status', '!=', 'cancelled') ->withCount('items') ->get(); if ($existingReceipts->isNotEmpty()) { $warnings[] = [ 'level' => 'high', 'type' => 'same_po', 'title' => '同一採購單已有進貨紀錄', 'message' => "此採購單已被引用於 {$existingReceipts->count()} 筆進貨單中,請確認是否為分批交貨。", 'related_receipts' => $existingReceipts->map(fn($r) => [ 'id' => $r->id, 'code' => $r->code, 'received_date' => $r->received_date->format('Y-m-d'), 'status' => match($r->status) { 'completed' => '已完成', 'draft' => '草稿', 'pending_audit' => '待審核', 'rejected' => '已退回', default => $r->status }, 'item_count' => $r->items_count, ]), ]; } } // 2. 近期同品項檢查 (針對每一個 item) $duplicatedItems = []; $recentDate = Carbon::now()->subDays($daysRange); foreach ($items as $item) { $productId = $item['product_id']; $qty = (float)$item['quantity_received']; // 尋找近期同供應商、同品項的進貨紀錄 $recentHits = GoodsReceiptItem::whereHas('goodsReceipt', function($query) use ($vendorId, $recentDate) { $query->where('vendor_id', $vendorId) ->where('received_date', '>=', $recentDate) ->where('status', '!=', 'cancelled'); }) ->where('product_id', $productId) ->with(['goodsReceipt:id,code,received_date']) ->get(); foreach ($recentHits as $hit) { $hitQty = (float)$hit->quantity_received; // 如果數量完全相同,視為高風險 $isExactMatch = abs($hitQty - $qty) < 0.001; $duplicatedItems[] = [ 'product_id' => $productId, 'product_name' => $item['product_name'] ?? '未知商品', 'last_receipt_code' => $hit->goodsReceipt->code, 'last_receipt_date' => $hit->goodsReceipt->received_date->format('Y-m-d'), 'last_quantity' => $hitQty, 'current_quantity' => $qty, 'is_high_risk' => $isExactMatch ]; } } if (!empty($duplicatedItems)) { // 按商品分組顯示 $warnings[] = [ 'level' => collect($duplicatedItems)->contains('is_high_risk', true) ? 'high' : 'medium', 'type' => 'recent_duplicate_product', 'title' => '近期同供應商商品進貨紀錄', 'message' => "偵測到以下商品在近 {$daysRange} 天內有過進貨紀錄,請確認是否重複收貨。", 'duplicated_items' => $duplicatedItems, ]; } // 3. 長期未調整單價檢查 (90 天內同供應商、同品項單價未變動) $stalePriceItems = $this->checkStalePrice($items, $vendorId); if (!empty($stalePriceItems)) { $warnings[] = [ 'level' => 'medium', 'type' => 'stale_price', 'title' => '長期未調整進貨單價', 'message' => '以下商品的進貨單價在過去 90 天內未曾變動,建議確認是否需要重新議價。', 'stale_items' => $stalePriceItems, ]; } return [ 'has_warnings' => !empty($warnings), 'warnings' => $warnings, ]; } /** * 檢查長期未調整單價的品項 * * 邏輯:針對每個品項,查詢同供應商在過去 90 天內的所有進貨單價。 * 若所有紀錄的單價皆相同,且至少有 1 筆以上紀錄,則視為「未調整」。 * * @param array $items 本次進貨的品項列表 * @param mixed $vendorId 供應商 ID * @return array */ private function checkStalePrice(array $items, $vendorId): array { // dump("Entering checkStalePrice", count($items), $vendorId); $staleDays = 90; $sinceDate = Carbon::now()->subDays($staleDays); $staleItems = []; foreach ($items as $item) { $productId = (int) $item['product_id']; $currentPrice = (float) ($item['unit_price'] ?? 0); // 查詢過去 90 天內的所有進貨單品項 $query = GoodsReceiptItem::whereHas('goodsReceipt', function ($query) use ($vendorId, $sinceDate) { if ($vendorId) { $query->where('vendor_id', $vendorId); } $query->where('received_date', '>=', $sinceDate->format('Y-m-d')) ->where('status', '!=', 'cancelled'); }) ->where('product_id', $productId) ->with(['goodsReceipt:id,code,received_date']) ->orderBy('id', 'desc'); $historicalPrices = $query->get(); // dump("Prod $productId count: " . $historicalPrices->count()); // 至少需要 1 筆歷史紀錄 if ($historicalPrices->count() < 1) { continue; } // 檢查所有歷史單價是否完全相同 (排除 Decimal 物件比對問題) $prices = $historicalPrices->map(fn($h) => round((float)$h->unit_price, 2)); $distinctPricesCount = $prices->unique()->count(); if ($distinctPricesCount > 1) { continue; // 歷史上有變動過 } $historicalPrice = $prices->first(); // 本次單價也跟歷史相同 (容許小數差距) if (round($currentPrice, 2) === round($historicalPrice, 2)) { $oldestReceipt = $historicalPrices->last(); $newestReceipt = $historicalPrices->first(); $staleItems[] = [ 'product_id' => $productId, 'product_name' => $item['product_name'] ?? '未知商品', 'unit_price' => $currentPrice, 'record_count' => $historicalPrices->count(), 'earliest_date' => $oldestReceipt->goodsReceipt->received_date->format('Y-m-d'), 'latest_date' => $newestReceipt->goodsReceipt->received_date->format('Y-m-d'), 'latest_code' => $newestReceipt->goodsReceipt->code, ]; } } return $staleItems; } }