|
|
|
@@ -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 }) |
|
|
|
} |
|
|
|
} |
|
|
|
|