|
|
|
@@ -1517,39 +1517,21 @@ open class ProductProcessService( |
|
|
|
|
|
|
|
val trimmedItemCode = itemCode?.trim()?.takeIf { it.isNotBlank() } |
|
|
|
val trimmedJobOrderCode = jobOrderCode?.trim()?.takeIf { it.isNotBlank() } |
|
|
|
|
|
|
|
// 1) 找出候选 jobOrder:用 date/itemCode/jobOrderCode/bomIds/type 先把数据缩小到今天等范围 |
|
|
|
val candidateProcesses = if (date != null) { |
|
|
|
productProcessRepository.findAllByDeletedIsFalseAndDate(date) |
|
|
|
} else { |
|
|
|
productProcessRepository.findAllByDeletedIsFalse() |
|
|
|
} |
|
|
|
|
|
|
|
val filteredCandidateProcesses = candidateProcesses.filter { p -> |
|
|
|
if (p.jobOrder?.isHidden == true) return@filter false |
|
|
|
val normalizedType = bomType?.trim()?.lowercase()?.takeIf { it.isNotBlank() } |
|
|
|
if (normalizedType != null) { |
|
|
|
val currentType = p.bom?.type?.trim()?.lowercase() |
|
|
|
if (currentType != normalizedType) return@filter false |
|
|
|
} |
|
|
|
if (trimmedItemCode != null) { |
|
|
|
val code = p.item?.code |
|
|
|
if (code == null || !code.contains(trimmedItemCode, ignoreCase = true)) return@filter false |
|
|
|
} |
|
|
|
if (trimmedJobOrderCode != null) { |
|
|
|
val code = p.jobOrder?.code |
|
|
|
if (code == null || !code.contains(trimmedJobOrderCode, ignoreCase = true)) return@filter false |
|
|
|
} |
|
|
|
if (bomIds != null && bomIds.isNotEmpty()) { |
|
|
|
val bid = p.bom?.id |
|
|
|
if (bid == null || !bomIds.contains(bid)) return@filter false |
|
|
|
} |
|
|
|
true |
|
|
|
} |
|
|
|
|
|
|
|
val candidateJobOrderIds = filteredCandidateProcesses |
|
|
|
.mapNotNull { it.jobOrder?.id } |
|
|
|
.distinct() |
|
|
|
val normalizedType = bomType?.trim()?.lowercase()?.takeIf { it.isNotBlank() } |
|
|
|
val normalizedBomIds = bomIds?.distinct().orEmpty() |
|
|
|
val useBomIds = normalizedBomIds.isNotEmpty() |
|
|
|
val bomIdsForQuery = if (useBomIds) normalizedBomIds else listOf(-1L) |
|
|
|
|
|
|
|
// 1) DB 端先完成候選 jobOrder 篩選 + 分組排序,避免全表拉回 JVM |
|
|
|
val candidateAggregates = productProcessRepository.findCandidateJobOrderAggregates( |
|
|
|
date = date, |
|
|
|
itemCode = trimmedItemCode, |
|
|
|
jobOrderCode = trimmedJobOrderCode, |
|
|
|
bomType = normalizedType, |
|
|
|
useBomIds = useBomIds, |
|
|
|
bomIds = bomIdsForQuery, |
|
|
|
) |
|
|
|
val candidateJobOrderIds = candidateAggregates.map { it.jobOrderId } |
|
|
|
|
|
|
|
if (candidateJobOrderIds.isEmpty()) { |
|
|
|
return JobOrderProductProcessPageResponse( |
|
|
|
@@ -1560,29 +1542,22 @@ open class ProductProcessService( |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
// 2) 把候选 jobOrder 下全部 productProcess / lines 取出来用于 qcReady 判断 |
|
|
|
val allCandidateProcesses = productProcessRepository.findByJobOrder_IdInAndDeletedIsFalse(candidateJobOrderIds) |
|
|
|
val allCandidateProcessIds = allCandidateProcesses.mapNotNull { it.id } |
|
|
|
|
|
|
|
val lines = if (allCandidateProcessIds.isNotEmpty()) { |
|
|
|
productProcessLineRepository.findByProductProcess_IdInWithOperatorAndEquipment(allCandidateProcessIds) |
|
|
|
} else { |
|
|
|
emptyList() |
|
|
|
} |
|
|
|
// 2) 批量查詢替代 N+1:stockIn / line 狀態都一次取回 |
|
|
|
val stockInLineByJobOrderId = stockInLineRepository |
|
|
|
.findAllByJobOrder_IdInAndDeletedFalse(candidateJobOrderIds) |
|
|
|
.groupBy { it.jobOrder?.id } |
|
|
|
.mapNotNull { (jobOrderId, rows) -> |
|
|
|
if (jobOrderId == null) null else jobOrderId to rows.firstOrNull() |
|
|
|
} |
|
|
|
.toMap() |
|
|
|
|
|
|
|
val linesByProcessId = lines.groupBy { it.productProcess.id ?: 0L } |
|
|
|
val processesByJobOrderId = allCandidateProcesses |
|
|
|
.filter { it.jobOrder?.id != null } |
|
|
|
.groupBy { it.jobOrder!!.id!! } |
|
|
|
val lineStatusByJobOrderId = productProcessLineRepository |
|
|
|
.countLineStatusByJobOrderIds(candidateJobOrderIds) |
|
|
|
.associateBy { it.jobOrderId } |
|
|
|
|
|
|
|
val jobOrders = jobOrderRepository.findAllById(candidateJobOrderIds) |
|
|
|
val jobOrderById = jobOrders.associateBy { it.id } |
|
|
|
val putawayFilter = putawayStatus?.trim()?.lowercase() |
|
|
|
val allowPutaway = includePutaway == true |
|
|
|
|
|
|
|
val stockInLineByJobOrderId = candidateJobOrderIds.associateWith { jobOrderId -> |
|
|
|
stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(jobOrderId) |
|
|
|
} |
|
|
|
|
|
|
|
// 3) 计算每个 jobOrder 的 qcReady,并做排序/分页 |
|
|
|
data class JobOrderQcKey( |
|
|
|
val jobOrderId: Long, |
|
|
|
val maxDate: LocalDate, |
|
|
|
@@ -1590,53 +1565,36 @@ open class ProductProcessService( |
|
|
|
val ready: Boolean |
|
|
|
) |
|
|
|
|
|
|
|
val qcKeys = candidateJobOrderIds.mapNotNull { jobOrderId -> |
|
|
|
val jobOrder = jobOrderById[jobOrderId] ?: return@mapNotNull null |
|
|
|
if (jobOrder.status == JobOrderStatus.PLANNING) return@mapNotNull null |
|
|
|
|
|
|
|
val processes = processesByJobOrderId[jobOrderId].orEmpty() |
|
|
|
if (processes.isEmpty()) return@mapNotNull null |
|
|
|
|
|
|
|
val jobOrderLines = processes.flatMap { p -> |
|
|
|
linesByProcessId[p.id ?: 0L].orEmpty() |
|
|
|
} |
|
|
|
|
|
|
|
val allLinesDone = jobOrderLines.isNotEmpty() && |
|
|
|
jobOrderLines.all { it.status == "Completed" || it.status == "Pass" } |
|
|
|
|
|
|
|
val qcKeys = candidateAggregates.mapNotNull { agg -> |
|
|
|
val jobOrderId = agg.jobOrderId |
|
|
|
val stockInLine = stockInLineByJobOrderId[jobOrderId] |
|
|
|
val stockStatus = stockInLine?.status |
|
|
|
//val stockInEligibleForQc = stockStatus != "completed" && stockStatus != "rejected" |
|
|
|
val allowPutaway = includePutaway == true |
|
|
|
// 列表只排除 completed/rejected(和 planning),null stockInLine 也会显示在 tab0 |
|
|
|
val stockStatus = stockInLine?.status?.trim()?.lowercase() |
|
|
|
|
|
|
|
val includedInList = |
|
|
|
// rejected 一般還是要排除(維持原規則) |
|
|
|
stockStatus != "rejected" && |
|
|
|
// completed 只有在不允許 putaway 時才排除 |
|
|
|
(allowPutaway || stockStatus != "completed") |
|
|
|
stockStatus != "rejected" && |
|
|
|
(allowPutaway || stockStatus != "completed") |
|
|
|
|
|
|
|
val putawayFilter = putawayStatus?.trim()?.lowercase() |
|
|
|
val includedByPutawayStatus = |
|
|
|
when (putawayFilter) { |
|
|
|
null, "", "all" -> true |
|
|
|
"completed" -> stockStatus?.lowercase() == "completed" |
|
|
|
"notcompleted", "not_completed", "not-completed", "waiting" -> stockStatus?.lowercase() != "completed" |
|
|
|
"completed" -> stockStatus == "completed" |
|
|
|
"notcompleted", "not_completed", "not-completed", "waiting" -> stockStatus != "completed" |
|
|
|
else -> true |
|
|
|
} |
|
|
|
val ready = includedInList && allLinesDone && stockInLine != null |
|
|
|
|
|
|
|
if (!includedInList || !includedByPutawayStatus) { |
|
|
|
return@mapNotNull null |
|
|
|
} |
|
|
|
if (!includedInList || !includedByPutawayStatus) return@mapNotNull null |
|
|
|
|
|
|
|
val maxDate = processes.mapNotNull { it.date }.maxOrNull() ?: LocalDate.MIN |
|
|
|
val minPriority = processes.mapNotNull { it.productionPriority }.minOrNull() ?: Int.MAX_VALUE |
|
|
|
val lineAggregate = lineStatusByJobOrderId[jobOrderId] |
|
|
|
val totalLines = lineAggregate?.totalLines ?: 0L |
|
|
|
val doneLines = lineAggregate?.doneLines ?: 0L |
|
|
|
val allLinesDone = totalLines > 0 && doneLines == totalLines |
|
|
|
val ready = includedInList && allLinesDone && stockInLine != null |
|
|
|
|
|
|
|
JobOrderQcKey( |
|
|
|
jobOrderId = jobOrderId, |
|
|
|
maxDate = maxDate, |
|
|
|
minPriority = minPriority, |
|
|
|
ready = ready |
|
|
|
maxDate = agg.maxDate ?: LocalDate.MIN, |
|
|
|
minPriority = agg.minPriority ?: Int.MAX_VALUE, |
|
|
|
ready = ready, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
@@ -1656,59 +1614,79 @@ open class ProductProcessService( |
|
|
|
val pagedJobOrderIds = sortedQcKeys.subList(from, to).map { it.jobOrderId } |
|
|
|
val pagedJobOrderIdSet = pagedJobOrderIds.toSet() |
|
|
|
|
|
|
|
// 4) 构造 content:返回 paged jobOrders 下的全部 productProcess |
|
|
|
val pickOrdersByJobOrderId = pagedJobOrderIds.associateWith { joId -> |
|
|
|
pickOrderRepository.findAllByJobOrder_Id(joId).firstOrNull() |
|
|
|
} |
|
|
|
val joPickOrdersByPickOrderId = pickOrdersByJobOrderId.mapNotNull { (joId, pick) -> |
|
|
|
val pickId = pick?.id ?: return@mapNotNull null |
|
|
|
joId to pickId |
|
|
|
}.map { it.second }.distinct().associateWith { pickOrderId -> |
|
|
|
joPickOrderRepository.findByPickOrderId(pickOrderId) |
|
|
|
// 4) 只對當前頁資料做批量關聯查詢,並且排序/資料縮小盡量下推 DB |
|
|
|
val contentProcesses = productProcessRepository |
|
|
|
.findByJobOrder_IdInAndDeletedIsFalseOrderByDateDescProductionPriorityAsc(pagedJobOrderIds) |
|
|
|
.filter { p -> p.jobOrder?.id != null && pagedJobOrderIdSet.contains(p.jobOrder!!.id!!) } |
|
|
|
|
|
|
|
val contentProcessIds = contentProcesses.mapNotNull { it.id } |
|
|
|
val lines = if (contentProcessIds.isNotEmpty()) { |
|
|
|
productProcessLineRepository.findByProductProcess_IdInWithOperatorAndEquipment(contentProcessIds) |
|
|
|
} else { |
|
|
|
emptyList() |
|
|
|
} |
|
|
|
val linesByProcessId = lines.groupBy { it.productProcess.id ?: 0L } |
|
|
|
|
|
|
|
val jobOrderById = jobOrderRepository.findAllById(pagedJobOrderIds).associateBy { it.id } |
|
|
|
|
|
|
|
val timeNeedToCompleteByBomId = pagedJobOrderIds.flatMap { _ -> |
|
|
|
// placeholder, real cache built below |
|
|
|
emptyList<Long>() |
|
|
|
}.toSet() |
|
|
|
val pickOrderByJobOrderId = mutableMapOf<Long, com.ffii.fpsms.modules.pickOrder.entity.PickOrder>() |
|
|
|
pickOrderRepository |
|
|
|
.findAllByJobOrder_IdInAndDeletedFalseOrderByJobOrder_IdAscCreatedDesc(pagedJobOrderIds) |
|
|
|
.forEach { po -> |
|
|
|
val joId = po.jobOrder?.id ?: return@forEach |
|
|
|
pickOrderByJobOrderId.putIfAbsent(joId, po) |
|
|
|
} |
|
|
|
|
|
|
|
val pickOrderIds = pickOrderByJobOrderId.values.mapNotNull { it.id }.distinct() |
|
|
|
val joPickOrdersByPickOrderId = if (pickOrderIds.isNotEmpty()) { |
|
|
|
joPickOrderRepository.findByPickOrderIdIn(pickOrderIds) |
|
|
|
.filter { !it.deleted && it.pickOrderId != null } |
|
|
|
.groupBy { it.pickOrderId!! } |
|
|
|
} else { |
|
|
|
emptyMap() |
|
|
|
} |
|
|
|
|
|
|
|
// 缓存:按 bomId 计算 timeNeedToComplete |
|
|
|
val bomIdsInScope = allCandidateProcesses |
|
|
|
.filter { it.jobOrder?.id != null && pagedJobOrderIdSet.contains(it.jobOrder!!.id!!) } |
|
|
|
val bomIdsInScope = contentProcesses |
|
|
|
.mapNotNull { it.bom?.id } |
|
|
|
.distinct() |
|
|
|
|
|
|
|
val timeNeedToCompleteCache = bomIdsInScope.associateWith { bomId -> |
|
|
|
bomProcessRepository.findByBomId(bomId) |
|
|
|
.filter { !it.deleted } |
|
|
|
.sumOf { it.durationInMinute ?: 0 } |
|
|
|
val timeNeedToCompleteCache = if (bomIdsInScope.isNotEmpty()) { |
|
|
|
bomProcessRepository.sumDurationByBomIds(bomIdsInScope) |
|
|
|
.associate { it.bomId to it.totalMinutes.toInt() } |
|
|
|
} else { |
|
|
|
emptyMap() |
|
|
|
} |
|
|
|
|
|
|
|
// 缓存:Uom(itemUom -> uomConversion.udfudesc) |
|
|
|
val itemIdsInScope = allCandidateProcesses |
|
|
|
.filter { it.jobOrder?.id != null && pagedJobOrderIdSet.contains(it.jobOrder!!.id!!) } |
|
|
|
val itemIdsInScope = contentProcesses |
|
|
|
.mapNotNull { it.item?.id } |
|
|
|
.distinct() |
|
|
|
|
|
|
|
val uomByItemId = itemIdsInScope.associateWith { itemId -> |
|
|
|
val itemUom = itemUomRepository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(itemId) |
|
|
|
val bomUom = uomConversionRepository.findById(itemUom?.uom?.id ?: 0L).orElse(null) |
|
|
|
bomUom?.udfudesc |
|
|
|
val stockItemUoms = if (itemIdsInScope.isNotEmpty()) { |
|
|
|
itemUomRepository.findAllByItemIdInAndStockUnitIsTrueAndDeletedIsFalse(itemIdsInScope) |
|
|
|
} else { |
|
|
|
emptyList() |
|
|
|
} |
|
|
|
val itemUomByItemId = mutableMapOf<Long, com.ffii.fpsms.modules.master.entity.ItemUom>() |
|
|
|
stockItemUoms.forEach { iu -> |
|
|
|
val itemId = iu.item?.id ?: return@forEach |
|
|
|
itemUomByItemId.putIfAbsent(itemId, iu) |
|
|
|
} |
|
|
|
|
|
|
|
val contentProcesses = allCandidateProcesses |
|
|
|
.filter { p -> p.jobOrder?.id != null && pagedJobOrderIdSet.contains(p.jobOrder!!.id!!) } |
|
|
|
|
|
|
|
val sortedContentProcesses = contentProcesses.sortedWith( |
|
|
|
compareByDescending<ProductProcess> { it.date ?: LocalDate.MIN } |
|
|
|
.thenBy { it.productionPriority ?: Int.MAX_VALUE } |
|
|
|
) |
|
|
|
val uomIds = itemUomByItemId.values.mapNotNull { it.uom?.id }.distinct() |
|
|
|
val uomById = if (uomIds.isNotEmpty()) { |
|
|
|
uomConversionRepository.findAllByIdInAndDeletedFalse(uomIds).associateBy { it.id } |
|
|
|
} else { |
|
|
|
emptyMap() |
|
|
|
} |
|
|
|
val uomByItemId = itemUomByItemId.mapValues { (_, iu) -> uomById[iu.uom?.id]?.udfudesc } |
|
|
|
|
|
|
|
val content = sortedContentProcesses.map { productProcess -> |
|
|
|
val content = contentProcesses.map { productProcess -> |
|
|
|
val jobOrderId = productProcess.jobOrder?.id ?: 0L |
|
|
|
val jobOrder = jobOrderById[jobOrderId] |
|
|
|
val stockInLine = stockInLineByJobOrderId[jobOrderId] |
|
|
|
val pickOrder = pickOrdersByJobOrderId[jobOrderId] |
|
|
|
val pickOrder = pickOrderByJobOrderId[jobOrderId] |
|
|
|
val pickOrderId = pickOrder?.id |
|
|
|
val joPickOrdersList = if (pickOrderId != null) joPickOrdersByPickOrderId[pickOrderId].orEmpty() else emptyList() |
|
|
|
|
|
|
|
|