|
|
|
@@ -140,6 +140,7 @@ open class ProductProcessService( |
|
|
|
this.name = bomProcess.process?.name |
|
|
|
this.description = bomProcess.description |
|
|
|
this.equipmentType = bomProcess.equipment?.name |
|
|
|
this.isOriginal = true |
|
|
|
} |
|
|
|
productProcessLineRepository.save(line) |
|
|
|
println("➕ Service: Created line ${index + 1} - seq: ${line.seqNo}, name: ${line.name}") |
|
|
|
@@ -239,6 +240,7 @@ open class ProductProcessService( |
|
|
|
this.name = request.name |
|
|
|
this.description = request.description |
|
|
|
this.equipmentType = request.equipmentType |
|
|
|
this.isOriginal = false |
|
|
|
} |
|
|
|
|
|
|
|
val saved = productProcessLineRepository.save(line) |
|
|
|
@@ -587,14 +589,14 @@ val sufficientStockQty = bomMaterials |
|
|
|
else -> "" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return productProcesses.map { process -> |
|
|
|
|
|
|
|
val jobType = jobTypeRepository.findById(process.jobOrder?.jobTypeId?:0L).orElse(null) |
|
|
|
|
|
|
|
//val joPickOrders = joPickOrderRepository.findByJobOrderId(process.jobOrder?.id?:0L) |
|
|
|
println("jobType id ${process.jobOrder?.jobTypeId}") |
|
|
|
|
|
|
|
val newCreatedLineIds = findNewCreatedLineIds(process.id ?: 0L, bom?.id ?: 0L) |
|
|
|
ProductProcessInfo( |
|
|
|
id = process.id?:0, |
|
|
|
bomId = process.bom?.id?:0, |
|
|
|
@@ -667,7 +669,8 @@ val sufficientStockQty = bomMaterials |
|
|
|
outputFromProcessQty = line.outputFromProcessQty?:0, |
|
|
|
outputFromProcessUom = line.outputFromProcessUom?:"", |
|
|
|
startTime = line.startTime, |
|
|
|
endTime = line.endTime |
|
|
|
endTime = line.endTime, |
|
|
|
isOringinal = line.isOriginal?:false |
|
|
|
|
|
|
|
) |
|
|
|
}.sortedBy { it.seqNo }, |
|
|
|
@@ -762,8 +765,8 @@ val sufficientStockQty = bomMaterials |
|
|
|
val targetRatioN = targetItemUom.ratioN ?: BigDecimal.ONE |
|
|
|
val targetRatioD = targetItemUom.ratioD ?: BigDecimal.ONE |
|
|
|
|
|
|
|
val baseQty = stockQtyValue.multiply(sourceRatioN).divide(sourceRatioD, 2, RoundingMode.HALF_UP) |
|
|
|
val finalQty = baseQty.multiply(targetRatioD).divide(targetRatioN, 2, RoundingMode.HALF_UP) |
|
|
|
val baseQty = stockQtyValue.multiply(sourceRatioN).divide(sourceRatioD, 2, RoundingMode.UP) |
|
|
|
val finalQty = baseQty.multiply(targetRatioD).divide(targetRatioN, 2, RoundingMode.UP) |
|
|
|
|
|
|
|
println(" Calculation: baseQty = $stockQtyValue * $sourceRatioN / $sourceRatioD = $baseQty") |
|
|
|
println(" Calculation: finalQty = $baseQty * $targetRatioD / $targetRatioN = $finalQty") |
|
|
|
@@ -793,10 +796,83 @@ val sufficientStockQty = bomMaterials |
|
|
|
// Use the same baseUomName for stockBaseUom since it's the same base unit |
|
|
|
val baseStockUomName = baseUomName |
|
|
|
val baseStockShortUom = baseShortUom |
|
|
|
|
|
|
|
|
|
|
|
println("Final values - reqQty: ${line.reqQty?.toInt()}, baseReqQty: $baseReqQty, stockQty: $stockQty, baseStockQty: $baseStockQty") |
|
|
|
|
|
|
|
|
|
|
|
val reqStockQtyResult = if (reqUomId > 0 && stockUomId != null && stockUomId > 0 && reqUomId != stockUomId) { |
|
|
|
try { |
|
|
|
// Convert reqQty from reqUomId to stockUomId via baseUnit |
|
|
|
// First convert to baseUnit |
|
|
|
val baseReqQtyForStock = if (baseReqQtyResult != null) { |
|
|
|
baseReqQtyResult.newQty |
|
|
|
} else { |
|
|
|
// If baseReqQty conversion failed, try again |
|
|
|
try { |
|
|
|
itemUomService.convertUomByItem( |
|
|
|
ConvertUomByItemRequest( |
|
|
|
itemId = itemId, |
|
|
|
qty = line.reqQty ?: BigDecimal.ZERO, |
|
|
|
uomId = reqUomId, |
|
|
|
targetUnit = "baseUnit" |
|
|
|
) |
|
|
|
).newQty |
|
|
|
} catch (e: Exception) { |
|
|
|
println("Error converting reqQty to baseUnit for stock conversion: ${e.message}") |
|
|
|
null |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (baseReqQtyForStock != null) { |
|
|
|
// Now convert from baseUnit to stockUomId |
|
|
|
val sourceItemUom = itemUomService.findBaseUnitByItemId(itemId) |
|
|
|
val targetItemUom = itemUomRepository.findByItemIdAndUomIdAndDeletedIsFalse(itemId, stockUomId) |
|
|
|
|
|
|
|
if (sourceItemUom != null && targetItemUom != null) { |
|
|
|
val sourceRatioN = sourceItemUom.ratioN ?: BigDecimal.ONE |
|
|
|
val sourceRatioD = sourceItemUom.ratioD ?: BigDecimal.ONE |
|
|
|
val targetRatioN = targetItemUom.ratioN ?: BigDecimal.ONE |
|
|
|
val targetRatioD = targetItemUom.ratioD ?: BigDecimal.ONE |
|
|
|
|
|
|
|
// Convert base to stock: baseQty * targetRatioD / targetRatioN |
|
|
|
val stockQtyDecimal = baseReqQtyForStock.multiply(targetRatioD).divide(targetRatioN, 2, RoundingMode.UP) |
|
|
|
val stockQty = stockQtyDecimal.setScale(0, RoundingMode.UP) // Round up to integer |
|
|
|
val stockUom = uomConversionRepository.findByIdAndDeletedFalse(stockUomId) |
|
|
|
|
|
|
|
println("Converting reqQty to stock unit: ${line.reqQty} (UOM id=$reqUomId) -> $stockQty (UOM id=$stockUomId)") |
|
|
|
|
|
|
|
ConvertUomByItemResponse( |
|
|
|
newQty = stockQty, |
|
|
|
udfudesc = stockUom?.udfudesc, |
|
|
|
udfShortDesc = stockUom?.udfShortDesc |
|
|
|
) |
|
|
|
} else { |
|
|
|
println("WARNING: Cannot convert reqQty to stock unit - missing ItemUom (sourceItemUom=${sourceItemUom != null}, targetItemUom=${targetItemUom != null})") |
|
|
|
null |
|
|
|
} |
|
|
|
} else { |
|
|
|
null |
|
|
|
} |
|
|
|
} catch (e: Exception) { |
|
|
|
println("Error converting reqQty to stock unit: ${e.message}") |
|
|
|
e.printStackTrace() |
|
|
|
null |
|
|
|
} |
|
|
|
} else if (reqUomId > 0 && stockUomId != null && stockUomId > 0 && reqUomId == stockUomId) { |
|
|
|
// If reqUomId and stockUomId are the same, no conversion needed |
|
|
|
val stockUom = uomConversionRepository.findByIdAndDeletedFalse(stockUomId) |
|
|
|
ConvertUomByItemResponse( |
|
|
|
newQty = line.reqQty ?: BigDecimal.ZERO, |
|
|
|
udfudesc = stockUom?.udfudesc, |
|
|
|
udfShortDesc = stockUom?.udfShortDesc |
|
|
|
) |
|
|
|
} else { |
|
|
|
println("Cannot convert reqQty to stock unit - reqUomId=$reqUomId, stockUomId=$stockUomId") |
|
|
|
null |
|
|
|
} |
|
|
|
|
|
|
|
val reqStockQty = reqStockQtyResult?.newQty?.toInt() ?: 0 |
|
|
|
|
|
|
|
println("Final values - reqQty: ${line.reqQty?.toInt()}, baseReqQty: $baseReqQty, stockQty: $stockQty, baseStockQty: $baseStockQty, reqStockQty: $reqStockQty") |
|
|
|
// Find BomProcessMaterial |
|
|
|
val bomProcessMaterial = bomMaterial?.id?.let { bomMaterialId -> |
|
|
|
bomProcessIds.firstNotNullOfOrNull { bomProcessId -> |
|
|
|
@@ -825,6 +901,7 @@ val sufficientStockQty = bomMaterials |
|
|
|
baseReqQty = baseReqQty, |
|
|
|
// Add this field if not exists |
|
|
|
stockQty = stockQty, |
|
|
|
stockReqQty = reqStockQty, |
|
|
|
baseStockQty = baseStockQty, // Add this field if not exists |
|
|
|
reqUom = uomName ?: "", |
|
|
|
reqBaseUom = baseUomName ?: "", |
|
|
|
@@ -879,6 +956,7 @@ val sufficientStockQty = bomMaterials |
|
|
|
this.processingTime = bomProcess.durationInMinute |
|
|
|
this.setupTime = bomProcess.prepTimeInMinute |
|
|
|
this.changeoverTime = bomProcess.postProdTimeInMinute |
|
|
|
this.isOriginal=true |
|
|
|
} |
|
|
|
productProcessLineRepository.save(productProcessLine) |
|
|
|
} |
|
|
|
@@ -1732,9 +1810,12 @@ val sufficientStockQty = bomMaterials |
|
|
|
this.defectDescription3 = sourceLine.defectDescription3 |
|
|
|
this.outputFromProcessQty = sourceLine.outputFromProcessQty |
|
|
|
this.outputFromProcessUom = sourceLine.outputFromProcessUom |
|
|
|
// 不复制时间字段,新 line 应该没有开始和结束时间 |
|
|
|
this.processingTime = sourceLine.processingTime |
|
|
|
this.setupTime = sourceLine.setupTime |
|
|
|
this.changeoverTime = sourceLine.changeoverTime |
|
|
|
this.startTime = null |
|
|
|
this.endTime = null |
|
|
|
this.isOriginal = false |
|
|
|
} |
|
|
|
|
|
|
|
// 保存新 line(原 line 的 seqNo 保持不变,不需要更新) |
|
|
|
@@ -1754,13 +1835,18 @@ open fun getJobProcessStatus(): List<JobProcessStatusResponse> { |
|
|
|
val productProcesses = productProcessRepository.findAllByDeletedIsFalse() |
|
|
|
.filter { it.status != ProductProcessStatus.COMPLETED } |
|
|
|
|
|
|
|
return productProcesses.map { process -> |
|
|
|
return productProcesses.mapNotNull { process -> |
|
|
|
val jobOrder = jobOrderRepository.findById(process.jobOrder?.id ?: 0L).orElse(null) |
|
|
|
|
|
|
|
// Filter out jobOrders in PLANNING status |
|
|
|
if (jobOrder?.status == JobOrderStatus.PLANNING) { |
|
|
|
return@mapNotNull null |
|
|
|
} |
|
|
|
|
|
|
|
val lines = productProcessLineRepository.findByProductProcess_Id(process.id ?: 0L) |
|
|
|
.sortedBy { it.seqNo } |
|
|
|
val bom=bomRepository.findById(process.bom?.id ?: 0L).orElse(null) |
|
|
|
|
|
|
|
//val equipmentDetail = equipmentDetailRepository.findById(equipmentDetailId).orElse(null) |
|
|
|
// Calculate planEndTime based on first start time + remaining processing time |
|
|
|
val firstStartTime = lines.firstOrNull { it.startTime != null }?.startTime |
|
|
|
val calculatedPlanEndTime = if (firstStartTime != null) { |
|
|
|
@@ -1786,15 +1872,34 @@ open fun getJobProcessStatus(): List<JobProcessStatusResponse> { |
|
|
|
jobOrderCode = jobOrder?.code ?: "", |
|
|
|
itemCode = process.item?.code ?: "", |
|
|
|
itemName = process.item?.name ?: "", |
|
|
|
status = process.status?.value ?: "", |
|
|
|
planEndTime = calculatedPlanEndTime, |
|
|
|
processes = (0 until 6).map { index -> |
|
|
|
if (index < lines.size) { |
|
|
|
val line = lines[index] |
|
|
|
val bomProcesses = bomProcessRepository.findByBomId(bom?.id ?: 0L).sortedBy { it.seqNo } |
|
|
|
val equipmentDetailId = line.equipmentDetailId |
|
|
|
|
|
|
|
// Use line's own data instead of indexing into bomProcesses |
|
|
|
val equipmentCode = when { |
|
|
|
equipmentDetailId != null -> { |
|
|
|
equipmentDetailRepository.findById(equipmentDetailId).orElse(null)?.code ?: "" |
|
|
|
} |
|
|
|
line.equipment?.code != null -> { |
|
|
|
line.equipment?.code ?: "" |
|
|
|
} |
|
|
|
else -> { |
|
|
|
// Safely access bomProcess - it might be deleted |
|
|
|
try { |
|
|
|
line.bomProcess?.equipment?.code ?: "" |
|
|
|
} catch (e: jakarta.persistence.EntityNotFoundException) { |
|
|
|
// BomProcess was deleted, fallback to equipmentType |
|
|
|
"" |
|
|
|
}.takeIf { it.isNotEmpty() } ?: (line.equipmentType ?: "") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
ProcessStatusInfo( |
|
|
|
|
|
|
|
equipmentCode = if(equipmentDetailId != null) equipmentDetailRepository.findById(equipmentDetailId).orElse(null)?.code ?: "" else bomProcesses[index].equipment?.code ?: "", |
|
|
|
equipmentCode = equipmentCode, |
|
|
|
startTime = line.startTime, |
|
|
|
endTime = line.endTime, |
|
|
|
processingTime = line.processingTime, |
|
|
|
@@ -1817,6 +1922,194 @@ open fun getJobProcessStatus(): List<JobProcessStatusResponse> { |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
private fun findNewCreatedLineIds(productProcessId: Long, bomId: Long): Set<Long> { |
|
|
|
// 获取 BOM 的所有 bomProcess,创建一个映射:bomProcessId -> seqNo |
|
|
|
val bomProcessMap = bomProcessRepository.findByBomId(bomId) |
|
|
|
.associate { it.id to (it.seqNo ?: 0L) } |
|
|
|
|
|
|
|
if (bomProcessMap.isEmpty()) { |
|
|
|
return emptySet() |
|
|
|
} |
|
|
|
|
|
|
|
// 获取所有 line,按 seqNo 排序 |
|
|
|
val allLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) |
|
|
|
.sortedBy { it.seqNo ?: 0L } |
|
|
|
|
|
|
|
println("=== findNewCreatedLineIds DEBUG START ===") |
|
|
|
println("BOM bomProcessMap: $bomProcessMap") |
|
|
|
println("All lines (sorted by seqNo):") |
|
|
|
allLines.forEach { line -> |
|
|
|
println(" id=${line.id}, seqNo=${line.seqNo}, bomProcessId=${line.bomProcess?.id}") |
|
|
|
} |
|
|
|
|
|
|
|
// 创建一个集合来跟踪哪些 line 是新创建的 |
|
|
|
val newCreatedLineIds = mutableSetOf<Long>() |
|
|
|
|
|
|
|
// 迭代检查,直到所有剩余的 line 都匹配 |
|
|
|
var iteration = 0 |
|
|
|
var hasChanges = true |
|
|
|
while (hasChanges) { |
|
|
|
iteration++ |
|
|
|
hasChanges = false |
|
|
|
|
|
|
|
println("\n--- Iteration $iteration ---") |
|
|
|
|
|
|
|
// 获取剩余的 line(排除已标记为新创建的),按 seqNo 排序 |
|
|
|
val remainingLines = allLines.filter { it.id !in newCreatedLineIds } |
|
|
|
.sortedBy { it.seqNo ?: 0L } |
|
|
|
|
|
|
|
println("Remaining lines (excluding new created):") |
|
|
|
remainingLines.forEach { line -> |
|
|
|
println(" id=${line.id}, seqNo=${line.seqNo}, bomProcessId=${line.bomProcess?.id}") |
|
|
|
} |
|
|
|
|
|
|
|
println("New created line IDs so far: $newCreatedLineIds") |
|
|
|
|
|
|
|
// 计算每个剩余 line 的期望 seqNo(应该是连续的 1, 2, 3...) |
|
|
|
val expectedSeqNoMap = remainingLines.mapIndexed { index, line -> |
|
|
|
line.id to (index + 1).toLong() |
|
|
|
}.toMap() |
|
|
|
|
|
|
|
println("Expected seqNo map:") |
|
|
|
expectedSeqNoMap.forEach { (lineId, expectedSeqNo) -> |
|
|
|
println(" lineId=$lineId -> expectedSeqNo=$expectedSeqNo") |
|
|
|
} |
|
|
|
|
|
|
|
// 检查每个剩余 line |
|
|
|
for (line in remainingLines) { |
|
|
|
val bomProcessId = line.bomProcess?.id |
|
|
|
val expectedSeqNo = expectedSeqNoMap[line.id] ?: continue |
|
|
|
|
|
|
|
println("\nChecking line id=${line.id}, seqNo=${line.seqNo}, bomProcessId=$bomProcessId, expectedSeqNo=$expectedSeqNo") |
|
|
|
|
|
|
|
if (bomProcessId == null) { |
|
|
|
println(" -> No bomProcessId, marking as new created") |
|
|
|
newCreatedLineIds.add(line.id ?: 0L) |
|
|
|
hasChanges = true |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
// 查找这个 bomProcessId 在 BOM 中的实际 seqNo |
|
|
|
val bomProcessSeqNo = bomProcessMap[bomProcessId] |
|
|
|
|
|
|
|
println(" -> BOM bomProcessId=$bomProcessId has seqNo=$bomProcessSeqNo in BOM") |
|
|
|
|
|
|
|
if (bomProcessSeqNo == null) { |
|
|
|
println(" -> bomProcessId not found in BOM, marking as new created") |
|
|
|
newCreatedLineIds.add(line.id ?: 0L) |
|
|
|
hasChanges = true |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
// 检查 line 的期望 seqNo 是否匹配 BOM 中 bomProcess 的 seqNo |
|
|
|
if (expectedSeqNo != bomProcessSeqNo) { |
|
|
|
println(" -> MISMATCH: expectedSeqNo=$expectedSeqNo != bomProcessSeqNo=$bomProcessSeqNo, marking as new created") |
|
|
|
newCreatedLineIds.add(line.id ?: 0L) |
|
|
|
hasChanges = true |
|
|
|
} else { |
|
|
|
println(" -> MATCH: expectedSeqNo=$expectedSeqNo == bomProcessSeqNo=$bomProcessSeqNo, keeping as original") |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
println("\n=== Final Result ===") |
|
|
|
println("New created line IDs: $newCreatedLineIds") |
|
|
|
println("=== findNewCreatedLineIds DEBUG END ===\n") |
|
|
|
|
|
|
|
return newCreatedLineIds |
|
|
|
} |
|
|
|
|
|
|
|
// 辅助函数:判断是否是原始创建的 line(使用缓存的结果) |
|
|
|
private fun isOriginalLine(productProcessLine: ProductProcessLine, newCreatedLineIds: Set<Long>): Boolean { |
|
|
|
val currentLineId = productProcessLine.id ?: 0L |
|
|
|
return currentLineId !in newCreatedLineIds |
|
|
|
} |
|
|
|
|
|
|
|
open fun deleteProductProcessLine(productProcessLineId: Long): MessageResponse { |
|
|
|
val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) |
|
|
|
?: return MessageResponse( |
|
|
|
id = productProcessLineId, |
|
|
|
code = "404", |
|
|
|
name = "ProductProcess Line Not Found", |
|
|
|
type = "error", |
|
|
|
message = "ProductProcess Line with ID $productProcessLineId not found", |
|
|
|
errorPosition = null, |
|
|
|
) |
|
|
|
|
|
|
|
val productProcessId = productProcessLine.productProcess?.id ?: return MessageResponse( |
|
|
|
id = productProcessLineId, |
|
|
|
code = "400", |
|
|
|
name = "Invalid ProductProcess", |
|
|
|
type = "error", |
|
|
|
message = "ProductProcess Line has no associated ProductProcess", |
|
|
|
errorPosition = null, |
|
|
|
) |
|
|
|
|
|
|
|
// 检查 JobOrder 状态 |
|
|
|
val productProcess = productProcessRepository.findById(productProcessId).orElse(null) |
|
|
|
val jobOrder = productProcess?.jobOrder |
|
|
|
if (jobOrder?.status != JobOrderStatus.PLANNING) { |
|
|
|
return MessageResponse( |
|
|
|
id = productProcessLineId, |
|
|
|
code = "400", |
|
|
|
name = "JobOrder Not In Planning", |
|
|
|
type = "error", |
|
|
|
message = "Cannot delete line when JobOrder is not in planning status", |
|
|
|
errorPosition = null, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
// 检查是否是原始 line(使用 ID 判断) |
|
|
|
val bomId = productProcess?.bom?.id ?: return MessageResponse( |
|
|
|
id = productProcessLineId, |
|
|
|
code = "400", |
|
|
|
name = "Invalid BOM", |
|
|
|
type = "error", |
|
|
|
message = "ProductProcess has no associated BOM", |
|
|
|
errorPosition = null, |
|
|
|
) |
|
|
|
|
|
|
|
// 获取新创建的 line IDs |
|
|
|
val newCreatedLineIds = findNewCreatedLineIds(productProcessId, bomId) |
|
|
|
|
|
|
|
// 检查是否是原始 line |
|
|
|
if (productProcessLine.isOriginal == true) { |
|
|
|
return MessageResponse( |
|
|
|
id = productProcessLineId, |
|
|
|
code = "400", |
|
|
|
name = "Cannot Delete Original Line", |
|
|
|
type = "error", |
|
|
|
message = "Cannot delete original process line. Only newly created lines can be deleted.", |
|
|
|
errorPosition = null, |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
val deletedSeqNo = productProcessLine.seqNo ?: 0L |
|
|
|
|
|
|
|
// 删除 line |
|
|
|
productProcessLineRepository.delete(productProcessLine) |
|
|
|
|
|
|
|
// 获取所有剩余的 lines,按 seqNo 排序,然后调整 seqNo |
|
|
|
val remainingLines = productProcessLineRepository.findByProductProcess_Id(productProcessId) |
|
|
|
.sortedBy { it.seqNo ?: 0L } |
|
|
|
|
|
|
|
// 更新所有 seqNo > deletedSeqNo 的 lines,将它们的 seqNo 减 1 |
|
|
|
remainingLines.filter { |
|
|
|
it.seqNo != null && |
|
|
|
it.seqNo!! > deletedSeqNo |
|
|
|
}.forEach { line -> |
|
|
|
line.seqNo = (line.seqNo ?: 0) - 1 |
|
|
|
productProcessLineRepository.save(line) |
|
|
|
} |
|
|
|
|
|
|
|
return MessageResponse( |
|
|
|
id = productProcessLineId, |
|
|
|
code = "200", |
|
|
|
name = "ProductProcess Line Deleted", |
|
|
|
type = "success", |
|
|
|
message = "ProductProcess Line deleted successfully", |
|
|
|
errorPosition = null, |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|