Просмотр исходного кода

New Goods Receipt Status Dashboard

reset-do-picking-order
B.E.N.S.O.N 2 недель назад
Родитель
Сommit
f5b3836770
2 измененных файлов: 126 добавлений и 24 удалений
  1. +117
    -23
      src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt
  2. +9
    -1
      src/main/java/com/ffii/fpsms/modules/dashboard/web/models/GoodsReceiptStatusResponse.kt

+ 117
- 23
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<Long> = mutableSetOf(),
val stockInLineIds: MutableSet<Long> = mutableSetOf(),
var itemsInspected: Int = 0,
var itemsWithIqcIssue: Int = 0,
var itemsCompletedPutAway: Int = 0
@@ -48,11 +54,17 @@ class GoodsReceiptStatusService(
.toSet()
}

val bySupplier = mutableMapOf<Long?, Agg>()
val byPurchaseOrder = mutableMapOf<Long?, Agg>()

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<GoodsReceiptStatusResponse> { it.supplierName }.thenBy { it.purchaseOrderCode })
}
}


+ 9
- 1
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
)


Загрузка…
Отмена
Сохранить