|
|
|
@@ -7,6 +7,7 @@ import com.ffii.fpsms.modules.productProcess.enums.ProductProcessStatus |
|
|
|
import com.ffii.fpsms.modules.productProcess.web.model.* |
|
|
|
import org.springframework.data.domain.Page |
|
|
|
import org.springframework.data.domain.Pageable |
|
|
|
import org.springframework.data.domain.PageRequest |
|
|
|
import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository |
|
|
|
import com.ffii.fpsms.modules.productProcess.entity.projections.jobOrderLineInfo |
|
|
|
import org.springframework.stereotype.Service |
|
|
|
@@ -1492,6 +1493,272 @@ open class ProductProcessService( |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* QC 列表:按 jobOrder 粒度分页,qcReady 由该 jobOrder 下所有 productProcessLine 是否全部 Completed/Pass 决定。 |
|
|
|
* |
|
|
|
* 注意:date/itemCode/jobOrderCode/bomIds 用来筛选“候选 jobOrder”,但 qcReady 判断会用该 jobOrder 下全部 productProcessLine。 |
|
|
|
*/ |
|
|
|
open fun searchJoborderProductProcessesPaged( |
|
|
|
date: LocalDate?, |
|
|
|
itemCode: String?, |
|
|
|
jobOrderCode: String?, |
|
|
|
bomIds: List<Long>?, |
|
|
|
qcReady: Boolean?, |
|
|
|
isDrink: Boolean?, |
|
|
|
page: Int, |
|
|
|
size: Int |
|
|
|
): JobOrderProductProcessPageResponse { |
|
|
|
val safeSize = if (size <= 0) 50 else size |
|
|
|
val safePage = if (page < 0) 0 else page |
|
|
|
|
|
|
|
val trimmedItemCode = itemCode?.trim()?.takeIf { it.isNotBlank() } |
|
|
|
val trimmedJobOrderCode = jobOrderCode?.trim()?.takeIf { it.isNotBlank() } |
|
|
|
|
|
|
|
// 1) 找出候选 jobOrder:用 date/itemCode/jobOrderCode/bomIds/isDrink 先把数据缩小到今天等范围 |
|
|
|
val candidateProcesses = if (date != null) { |
|
|
|
productProcessRepository.findAllByDeletedIsFalseAndDate(date) |
|
|
|
} else { |
|
|
|
productProcessRepository.findAllByDeletedIsFalse() |
|
|
|
} |
|
|
|
|
|
|
|
val filteredCandidateProcesses = candidateProcesses.filter { p -> |
|
|
|
if (isDrink != null) { |
|
|
|
val bomIsDrink = p.bom?.isDrink |
|
|
|
if (bomIsDrink != isDrink) 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() |
|
|
|
|
|
|
|
if (candidateJobOrderIds.isEmpty()) { |
|
|
|
return JobOrderProductProcessPageResponse( |
|
|
|
content = emptyList(), |
|
|
|
totalJobOrders = 0L, |
|
|
|
page = safePage, |
|
|
|
size = safeSize |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
// 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() |
|
|
|
} |
|
|
|
|
|
|
|
val linesByProcessId = lines.groupBy { it.productProcess.id ?: 0L } |
|
|
|
val processesByJobOrderId = allCandidateProcesses |
|
|
|
.filter { it.jobOrder?.id != null } |
|
|
|
.groupBy { it.jobOrder!!.id!! } |
|
|
|
|
|
|
|
val jobOrders = jobOrderRepository.findAllById(candidateJobOrderIds) |
|
|
|
val jobOrderById = jobOrders.associateBy { it.id } |
|
|
|
|
|
|
|
val stockInLineByJobOrderId = candidateJobOrderIds.associateWith { jobOrderId -> |
|
|
|
stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(jobOrderId) |
|
|
|
} |
|
|
|
|
|
|
|
// 3) 计算每个 jobOrder 的 qcReady,并做排序/分页 |
|
|
|
data class JobOrderQcKey( |
|
|
|
val jobOrderId: Long, |
|
|
|
val maxDate: LocalDate, |
|
|
|
val minPriority: Int, |
|
|
|
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 stockInLine = stockInLineByJobOrderId[jobOrderId] |
|
|
|
val stockStatus = stockInLine?.status |
|
|
|
val stockInEligibleForQc = stockStatus != "completed" && stockStatus != "rejected" |
|
|
|
|
|
|
|
// 列表只排除 completed/rejected(和 planning),null stockInLine 也会显示在 tab0 |
|
|
|
val includedInList = stockInEligibleForQc |
|
|
|
val ready = includedInList && allLinesDone && stockInLine != null |
|
|
|
|
|
|
|
if (!includedInList) { |
|
|
|
return@mapNotNull null |
|
|
|
} |
|
|
|
|
|
|
|
val maxDate = processes.mapNotNull { it.date }.maxOrNull() ?: LocalDate.MIN |
|
|
|
val minPriority = processes.mapNotNull { it.productionPriority }.minOrNull() ?: Int.MAX_VALUE |
|
|
|
|
|
|
|
JobOrderQcKey( |
|
|
|
jobOrderId = jobOrderId, |
|
|
|
maxDate = maxDate, |
|
|
|
minPriority = minPriority, |
|
|
|
ready = ready |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
val filteredQcKeys = qcKeys.filter { key -> qcReady == null || key.ready == qcReady } |
|
|
|
val totalJobOrders = filteredQcKeys.size.toLong() |
|
|
|
|
|
|
|
val sortedQcKeys = filteredQcKeys.sortedWith( |
|
|
|
compareByDescending<JobOrderQcKey> { it.maxDate }.thenBy { it.minPriority } |
|
|
|
) |
|
|
|
|
|
|
|
val from = safePage * safeSize |
|
|
|
if (from >= sortedQcKeys.size) { |
|
|
|
return JobOrderProductProcessPageResponse(emptyList(), totalJobOrders, safePage, safeSize) |
|
|
|
} |
|
|
|
|
|
|
|
val to = minOf(from + safeSize, sortedQcKeys.size) |
|
|
|
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) |
|
|
|
} |
|
|
|
|
|
|
|
val timeNeedToCompleteByBomId = pagedJobOrderIds.flatMap { _ -> |
|
|
|
// placeholder, real cache built below |
|
|
|
emptyList<Long>() |
|
|
|
}.toSet() |
|
|
|
|
|
|
|
// 缓存:按 bomId 计算 timeNeedToComplete |
|
|
|
val bomIdsInScope = allCandidateProcesses |
|
|
|
.filter { it.jobOrder?.id != null && pagedJobOrderIdSet.contains(it.jobOrder!!.id!!) } |
|
|
|
.mapNotNull { it.bom?.id } |
|
|
|
.distinct() |
|
|
|
|
|
|
|
val timeNeedToCompleteCache = bomIdsInScope.associateWith { bomId -> |
|
|
|
bomProcessRepository.findByBomId(bomId) |
|
|
|
.filter { !it.deleted } |
|
|
|
.sumOf { it.durationInMinute ?: 0 } |
|
|
|
} |
|
|
|
|
|
|
|
// 缓存:Uom(itemUom -> uomConversion.udfudesc) |
|
|
|
val itemIdsInScope = allCandidateProcesses |
|
|
|
.filter { it.jobOrder?.id != null && pagedJobOrderIdSet.contains(it.jobOrder!!.id!!) } |
|
|
|
.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 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 content = sortedContentProcesses.map { productProcess -> |
|
|
|
val jobOrderId = productProcess.jobOrder?.id ?: 0L |
|
|
|
val jobOrder = jobOrderById[jobOrderId] |
|
|
|
val stockInLine = stockInLineByJobOrderId[jobOrderId] |
|
|
|
val pickOrder = pickOrdersByJobOrderId[jobOrderId] |
|
|
|
val pickOrderId = pickOrder?.id |
|
|
|
val joPickOrdersList = if (pickOrderId != null) joPickOrdersByPickOrderId[pickOrderId].orEmpty() else emptyList() |
|
|
|
|
|
|
|
val productProcessLines = linesByProcessId[productProcess.id ?: 0L].orEmpty() |
|
|
|
val finishedCount = productProcessLines.count { it.status == "Completed" } |
|
|
|
|
|
|
|
val bomIsDrink = productProcess.bom?.isDrink |
|
|
|
val matchStatus = if (joPickOrdersList.isNotEmpty() && |
|
|
|
joPickOrdersList.all { it.matchStatus == JoPickOrderStatus.completed } |
|
|
|
) { |
|
|
|
"completed" |
|
|
|
} else if (joPickOrdersList.any { it.matchStatus == JoPickOrderStatus.scanned }) { |
|
|
|
"scanned" |
|
|
|
} else { |
|
|
|
"pending" |
|
|
|
} |
|
|
|
|
|
|
|
val bomId = productProcess.bom?.id |
|
|
|
val timeNeedToComplete = bomId?.let { timeNeedToCompleteCache[it] } ?: 0 |
|
|
|
val uom = productProcess.item?.id?.let { uomByItemId[it] } |
|
|
|
|
|
|
|
AllJoborderProductProcessInfoResponse( |
|
|
|
id = productProcess.id ?: 0L, |
|
|
|
productProcessCode = productProcess.productProcessCode, |
|
|
|
status = productProcess.status.value, |
|
|
|
startTime = productProcess.startTime, |
|
|
|
endTime = productProcess.endTime, |
|
|
|
isDrink = bomIsDrink, |
|
|
|
matchStatus = matchStatus, |
|
|
|
RequiredQty = jobOrder?.reqQty?.toInt() ?: 0, |
|
|
|
Uom = uom, |
|
|
|
lotNo = stockInLine?.lotNo, |
|
|
|
productionPriority = productProcess.productionPriority, |
|
|
|
date = productProcess.date, |
|
|
|
bomId = bomId, |
|
|
|
TimeNeedToComplete = timeNeedToComplete, |
|
|
|
assignedTo = pickOrder?.assignTo?.id, |
|
|
|
itemName = productProcess.item?.name, |
|
|
|
itemCode = productProcess.item?.code, |
|
|
|
pickOrderId = pickOrder?.id, |
|
|
|
pickOrderStatus = pickOrder?.status?.value, |
|
|
|
jobOrderId = productProcess.jobOrder?.id, |
|
|
|
stockInLineId = stockInLine?.id, |
|
|
|
jobOrderCode = jobOrder?.code, |
|
|
|
productProcessLineCount = productProcessLines.size, |
|
|
|
FinishedProductProcessLineCount = finishedCount, |
|
|
|
lines = productProcessLines.map { line -> |
|
|
|
ProductProcessInfoResponse( |
|
|
|
id = line.id ?: 0, |
|
|
|
operatorId = line.operator?.id, |
|
|
|
operatorName = line.operator?.name ?: "", |
|
|
|
equipmentId = line.equipment?.id, |
|
|
|
equipmentName = line.equipmentType ?: "", |
|
|
|
startTime = line.startTime, |
|
|
|
endTime = line.endTime, |
|
|
|
status = line.status ?: "" |
|
|
|
) |
|
|
|
} |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
return JobOrderProductProcessPageResponse( |
|
|
|
content = content, |
|
|
|
totalJobOrders = totalJobOrders, |
|
|
|
page = safePage, |
|
|
|
size = safeSize |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
open fun updateProductProcessLineStartTime(productProcessLineId: Long): MessageResponse { |
|
|
|
val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) |
|
|
|
productProcessLine.startTime = LocalDateTime.now() |
|
|
|
|