From f5b38367701c146146973a7d899e20e1d5460962 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Tue, 3 Mar 2026 16:38:32 +0800 Subject: [PATCH] New Goods Receipt Status Dashboard --- .../service/GoodsReceiptStatusService.kt | 140 +++++++++++++++--- .../web/models/GoodsReceiptStatusResponse.kt | 10 +- 2 files changed, 126 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt b/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt index 713ffd1..82376fa 100644 --- a/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt +++ b/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt @@ -1,7 +1,9 @@ package com.ffii.fpsms.modules.dashboard.service import com.ffii.fpsms.modules.dashboard.web.models.GoodsReceiptStatusResponse +import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository +import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderStatus import com.ffii.fpsms.modules.qc.entity.QcResultRepository import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import org.springframework.stereotype.Service @@ -10,14 +12,18 @@ import java.time.LocalDate @Service class GoodsReceiptStatusService( private val purchaseOrderRepository: PurchaseOrderRepository, + private val purchaseOrderLineRepository: PurchaseOrderLineRepository, private val stockInLineRepository: StockInLineRepository, private val qcResultRepository: QcResultRepository ) { private data class Agg( + val purchaseOrderId: Long?, + val purchaseOrderCode: String?, val supplierId: Long?, + val supplierCode: String?, val supplierName: String, var expectedNoOfDelivery: Int = 0, - val ordersReceivedAtDock: MutableSet = mutableSetOf(), + val stockInLineIds: MutableSet = mutableSetOf(), var itemsInspected: Int = 0, var itemsWithIqcIssue: Int = 0, var itemsCompletedPutAway: Int = 0 @@ -48,11 +54,17 @@ class GoodsReceiptStatusService( .toSet() } - val bySupplier = mutableMapOf() + val byPurchaseOrder = mutableMapOf() - fun upsertAgg(supplierId: Long?, supplierName: String): Agg { - return bySupplier.getOrPut(supplierId) { - Agg(supplierId = supplierId, supplierName = supplierName) + fun upsertAgg(poId: Long?, poCode: String?, supplierId: Long?, supplierCode: String?, supplierName: String): Agg { + return byPurchaseOrder.getOrPut(poId) { + Agg( + purchaseOrderId = poId, + purchaseOrderCode = poCode, + supplierId = supplierId, + supplierCode = supplierCode, + supplierName = supplierName + ) } } @@ -60,29 +72,33 @@ class GoodsReceiptStatusService( purchaseOrders.forEach { po -> val supplier = po.supplier val supplierId = supplier?.id + val supplierCode = supplier?.code val supplierName = supplier?.name ?: "N/A" - upsertAgg(supplierId, supplierName).expectedNoOfDelivery += 1 + val poId = po.id + val poCode = po.code + val agg = upsertAgg(poId, poCode, supplierId, supplierCode, supplierName) + agg.expectedNoOfDelivery = 1 } // Receiving / IQC / Put-away (by stock-in receipt date) stockInLines.forEach { sil -> - val supplier = sil.purchaseOrder?.supplier + val po = sil.purchaseOrder + val poId = po?.id + val poCode = po?.code + val supplier = po?.supplier val supplierId = supplier?.id + val supplierCode = supplier?.code val supplierName = supplier?.name ?: "N/A" - val agg = upsertAgg(supplierId, supplierName) - + + val agg = upsertAgg(poId, poCode, supplierId, supplierCode, supplierName) + val silId = sil.id ?: return@forEach - val po = sil.purchaseOrder - val poIdForExpected = po?.id - if (poIdForExpected != null && !todaysPoIds.contains(poIdForExpected) && extraPoIdsCounted.add(poIdForExpected)) { - agg.expectedNoOfDelivery += 1 - } - - // Orders received at dock: count orders with DN + (supplier) lot no entered - val poId = sil.purchaseOrder?.id - if (poId != null && !sil.dnNo.isNullOrBlank() && !sil.productLotNo.isNullOrBlank()) { - agg.ordersReceivedAtDock.add(poId) + // If PO not in today's list, mark as expected + if (poId != null && !todaysPoIds.contains(poId) && extraPoIdsCounted.add(poId)) { + agg.expectedNoOfDelivery = 1 + } else if (agg.expectedNoOfDelivery == 0) { + agg.expectedNoOfDelivery = 1 } // Items inspected: any IQC result recorded @@ -101,19 +117,97 @@ class GoodsReceiptStatusService( } } - return bySupplier.values + // For each purchase order, fetch ALL its stock-in lines (regardless of receipt date) + // and check if all have finished IQC + byPurchaseOrder.values.forEach { agg -> + if (agg.purchaseOrderId != null) { + val allStockInLines = stockInLineRepository.findAllByPurchaseOrderIdAndDeletedFalse(agg.purchaseOrderId) + allStockInLines.ifPresent { lines -> + lines.forEach { sil -> + val silId = sil.id + if (silId != null) { + agg.stockInLineIds.add(silId) + } + } + } + } + } + + // Get all stock-in line IDs from all purchase orders to check IQC status + val allPoStockInLineIds = byPurchaseOrder.values.flatMap { it.stockInLineIds }.toSet() + + // Check IQC status for ALL stock-in lines from all purchase orders + val allInspectedLineIdSet = if (allPoStockInLineIds.isEmpty()) { + emptySet() + } else { + qcResultRepository + .findDistinctStockInLineIdsByStockInLineIdInAndDeletedFalse(allPoStockInLineIds.toList()) + .toSet() + } + + // Build complete PO status map for all POs that appear in the aggregation + // This includes POs from both purchaseOrders (by estimated arrival date) and stockInLines (by receipt date) + val allPoIds = byPurchaseOrder.keys.filterNotNull().toSet() + val allPosWithStatus = allPoIds.mapNotNull { poId -> + purchaseOrderRepository.findById(poId).orElse(null)?.let { po -> poId to po.status } + } + val poStatusMap = allPosWithStatus.toMap() + + return byPurchaseOrder.values .map { agg -> + // Determine whether this PO should be hidden from the dashboard table: + // hide ONLY when *all items (POLs)* under this PO are fully handled, + // meaning each PO line has at least one stock-in line and + // all of its stock-in lines are completed or rejected. + val shouldHide = agg.purchaseOrderId?.let { poId -> + val poLines = purchaseOrderLineRepository.findAllByPurchaseOrderIdAndDeletedIsFalse(poId) + if (poLines.isEmpty()) { + false + } else { + poLines.all { pol -> + val sils = pol.stockInLines + sils.isNotEmpty() && sils.all { sil -> + val status = sil.status?.lowercase() ?: "" + status == "completed" || status == "rejected" + } + } + } + } ?: false + + // Order is processed ONLY if: + // 1. The purchase order status is COMPLETED (all PO lines finished and items put away) + // - This is controlled by /po/check/{id} -> PurchaseOrderService.checkPolAndCompletePo + // 2. Optionally, ALL stock-in lines for the purchase order have finished IQC (regardless of pass/fail) + // - We still check IQC to ensure data consistency, but PO status is the primary gate. + val totalStockInLines = agg.stockInLineIds.size + val finishedIqcCount = agg.stockInLineIds.count { silId -> silId in allInspectedLineIdSet } + + val allItemsFinishedIqc = totalStockInLines > 0 && totalStockInLines == finishedIqcCount + + // PO is considered processed only when business marks it COMPLETED. + val poCompleted = agg.purchaseOrderId + ?.let { poStatusMap[it] == PurchaseOrderStatus.COMPLETED } + ?: false + + val receivedCount = if (poCompleted && allItemsFinishedIqc) 1 else 0 + val totalCount = agg.expectedNoOfDelivery + val statistics = "$receivedCount/$totalCount" + "張單已處理" + GoodsReceiptStatusResponse( supplierId = agg.supplierId, + supplierCode = agg.supplierCode, supplierName = agg.supplierName, + purchaseOrderCode = agg.purchaseOrderCode, + statistics = statistics, expectedNoOfDelivery = agg.expectedNoOfDelivery, - noOfOrdersReceivedAtDock = agg.ordersReceivedAtDock.size, + noOfOrdersReceivedAtDock = receivedCount, noOfItemsInspected = agg.itemsInspected, noOfItemsWithIqcIssue = agg.itemsWithIqcIssue, - noOfItemsCompletedPutAwayAtStore = agg.itemsCompletedPutAway + noOfItemsCompletedPutAwayAtStore = agg.itemsCompletedPutAway, + hideFromDashboard = shouldHide ) } - .sortedBy { it.supplierName } + .sortedWith(compareBy { it.supplierName }.thenBy { it.purchaseOrderCode }) } } diff --git a/src/main/java/com/ffii/fpsms/modules/dashboard/web/models/GoodsReceiptStatusResponse.kt b/src/main/java/com/ffii/fpsms/modules/dashboard/web/models/GoodsReceiptStatusResponse.kt index 3ad59cf..593c09b 100644 --- a/src/main/java/com/ffii/fpsms/modules/dashboard/web/models/GoodsReceiptStatusResponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/dashboard/web/models/GoodsReceiptStatusResponse.kt @@ -2,11 +2,19 @@ package com.ffii.fpsms.modules.dashboard.web.models data class GoodsReceiptStatusResponse( val supplierId: Long?, + val supplierCode: String?, val supplierName: String, + val purchaseOrderCode: String?, + val statistics: String, val expectedNoOfDelivery: Int, val noOfOrdersReceivedAtDock: Int, val noOfItemsInspected: Int, val noOfItemsWithIqcIssue: Int, - val noOfItemsCompletedPutAwayAtStore: Int + val noOfItemsCompletedPutAwayAtStore: Int, + /** + * When true, this purchase order should be hidden from the dashboard table, + * but still counted in the overall statistics (訂單已處理). + */ + val hideFromDashboard: Boolean = false )