diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 7b196de..5d558b9 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -68,6 +68,7 @@ import java.time.LocalDate import java.time.LocalDateTime import com.ffii.fpsms.modules.master.entity.BomMaterialRepository import com.ffii.fpsms.modules.master.service.ItemUomService +import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest @Service open class JobOrderService( val jobOrderRepository: JobOrderRepository, @@ -208,7 +209,6 @@ open class JobOrderService( return RecordsRes(records, total.toInt()); } - // 添加辅助方法计算库存统计 private fun calculateStockCounts( jobOrder: JobOrder, inventoriesMap: Map @@ -216,7 +216,7 @@ open class JobOrderService( // 过滤掉 consumables 和 CMB 类型的物料 val nonConsumablesJobms = jobOrder.jobms.filter { jobm -> val itemType = jobm.item?.type?.lowercase() - itemType != "consumables" && itemType != "cmb"&& itemType != "nm" + itemType != "consumables" && itemType != "consumable" && itemType != "cmb" && itemType != "nm" } if (nonConsumablesJobms.isEmpty()) { @@ -226,14 +226,16 @@ open class JobOrderService( var sufficientCount = 0 var insufficientCount = 0 + println("=== JobOrderService.calculateStockCounts for JobOrder: ${jobOrder.code} ===") + nonConsumablesJobms.forEach { jobm -> val itemId = jobm.item?.id - val reqQty = jobm.reqQty ?: BigDecimal.ZERO + val itemCode = jobm.item?.code ?: "N/A" + val itemName = jobm.item?.name ?: "N/A" if (itemId != null) { val inventory = inventoriesMap[itemId] val availableQty = if (inventory != null) { - // 使用 availableQty,如果没有则计算:onHandQty - onHoldQty - unavailableQty inventory.availableQty ?: ( (inventory.onHandQty ?: BigDecimal.ZERO) - (inventory.onHoldQty ?: BigDecimal.ZERO) - @@ -243,17 +245,74 @@ open class JobOrderService( BigDecimal.ZERO } - if (availableQty >= reqQty) { + // ✅ 获取 reqQty 和 availableQty 的单位信息 + val reqQty = jobm.reqQty ?: BigDecimal.ZERO + val reqUomId = jobm.uom?.id ?: 0L + val reqUomName = jobm.uom?.udfudesc ?: "N/A" + + // ✅ 修复:使用 itemUomService 获取 stockUomId(与 ProductProcessService 保持一致) + val stockUnitItemUom = itemUomService.findStockUnitByItemId(itemId) + val stockUomId = stockUnitItemUom?.uom?.id ?: 0L + val stockUomName = stockUnitItemUom?.uom?.udfudesc ?: "N/A" + + // ✅ 转换为 base unit 进行比较(与 ProductProcessService 保持一致) + val baseReqQtyResult = if (reqUomId > 0 && reqQty > BigDecimal.ZERO) { + try { + itemUomService.convertUomByItem( + ConvertUomByItemRequest( + itemId = itemId, + qty = reqQty, + uomId = reqUomId, + targetUnit = "baseUnit" + ) + ) + } catch (e: Exception) { + println("Error converting reqQty to base unit: ${e.message}") + null + } + } else { + null + } + + val baseAvailableQtyResult = if (stockUomId > 0 && availableQty > BigDecimal.ZERO) { + try { + itemUomService.convertUomByItem( + ConvertUomByItemRequest( + itemId = itemId, + qty = availableQty, + uomId = stockUomId, + targetUnit = "baseUnit" + ) + ) + } catch (e: Exception) { + println("Error converting availableQty to base unit: ${e.message}") + null + } + } else { + null + } + + val baseReqQty = baseReqQtyResult?.newQty ?: BigDecimal.ZERO + val baseAvailableQty = baseAvailableQtyResult?.newQty ?: BigDecimal.ZERO + val baseUomName = baseReqQtyResult?.udfudesc ?: baseAvailableQtyResult?.udfudesc ?: "N/A" + + // ✅ 使用 base unit 进行比较 + if (baseAvailableQty >= baseReqQty) { sufficientCount++ + println("✅ SUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") } else { insufficientCount++ + println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") } } else { // 如果没有 itemId,视为不足 insufficientCount++ + println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId") } } + println("=== Result: sufficient=$sufficientCount, insufficient=$insufficientCount ===") + return Pair(sufficientCount, insufficientCount) } open fun jobOrderDetail(id: Long): JobOrderDetail { diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt index 31315d0..092c4c9 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt @@ -757,8 +757,8 @@ open class BomService( // 创建 equipment equipment = Equipment().apply { - this.name = equipmentName // 完整值 XXX-YYY - this.code = secondPart + this.code = equipmentName // 完整值 XXX-YYY + this.name = secondPart this.description = firstPart // XXX 写入 description } equipment = equipmentRepository.saveAndFlush(equipment) diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt index 695a2cc..15979b2 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt @@ -74,9 +74,23 @@ open class PickExecutionIssueService( @Value("\${pick.execution.auto-resuggest-on-rejection:false}") private val autoResuggestOnLotRejection: Boolean = false - @Transactional(rollbackFor = [Exception::class]) open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse { try { + println("=== recordPickExecutionIssue: START ===") + println("Request details:") + println(" pickOrderId: ${request.pickOrderId}") + println(" pickOrderLineId: ${request.pickOrderLineId}") + println(" itemId: ${request.itemId}") + println(" itemCode: ${request.itemCode}") + println(" lotId: ${request.lotId}") + println(" lotNo: ${request.lotNo}") + println(" requiredQty: ${request.requiredQty}") + println(" actualPickQty: ${request.actualPickQty}") + println(" missQty: ${request.missQty}") + println(" badItemQty: ${request.badItemQty}") + println(" badReason: ${request.badReason}") + println(" issueCategory: ${request.issueCategory}") + println("========================================") // 1. 检查是否已经存在相同的 pick execution issue 记录 val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( @@ -84,7 +98,14 @@ open class PickExecutionIssueService( request.lotId ?: 0L ) + println("Checking for existing issues...") + println(" Found ${existingIssues.size} existing issues") + existingIssues.forEachIndexed { index, issue -> + println(" Existing[$index]: id=${issue.id}, issueNo=${issue.issueNo}, handleStatus=${issue.handleStatus}, issueQty=${issue.issueQty}") + } + if (existingIssues.isNotEmpty()) { + println("❌ Duplicate issue found - returning DUPLICATE error") return MessageResponse( id = null, name = "Pick execution issue already exists", @@ -96,18 +117,24 @@ open class PickExecutionIssueService( } val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null) + println("Pick order: id=${pickOrder?.id}, code=${pickOrder?.code}, type=${pickOrder?.type?.value}") // 2. 获取 inventory_lot_line 并计算账面数量 (bookQty) val inventoryLotLine = request.lotId?.let { inventoryLotLineRepository.findById(it).orElse(null) } + println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}") + // 计算账面数量(创建 issue 时的快照) val bookQty = if (inventoryLotLine != null) { val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO - inQty.subtract(outQty) // bookQty = inQty - outQty + val calculated = inQty.subtract(outQty) // bookQty = inQty - outQty + println(" BookQty calculation: inQty=$inQty, outQty=$outQty, bookQty=$calculated") + calculated } else { + println(" No inventory lot line found, bookQty=0") BigDecimal.ZERO } @@ -117,41 +144,53 @@ open class PickExecutionIssueService( val missQty = request.missQty ?: BigDecimal.ZERO val badItemQty = request.badItemQty ?: BigDecimal.ZERO val badReason = request.badReason ?: "quantity_problem" + + println("=== Quantity Summary ===") + println(" Required Qty: $requiredQty") + println(" Actual Pick Qty: $actualPickQty") + println(" Miss Qty: $missQty") + println(" Bad Item Qty: $badItemQty") + println(" Bad Reason: $badReason") + println(" Book Qty: $bookQty") + // 4. 计算 issueQty(实际的问题数量) val issueQty = when { // 情况1: 已拣完但有坏品 actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { + println(" Case 1: actualPickQty == requiredQty && badItemQty > 0") + println(" issueQty = badItemQty = $badItemQty") badItemQty // issueQty = badItemQty } badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> { + println(" Case 2: badReason == 'package_problem' && badItemQty > 0") + println(" issueQty = badItemQty = $badItemQty") badItemQty } - actualPickQty < requiredQty -> { + println(" Case 3: actualPickQty < requiredQty") val calculatedIssueQty = bookQty.subtract(actualPickQty) + println(" issueQty = bookQty - actualPickQty = $bookQty - $actualPickQty = $calculatedIssueQty") if (missQty > BigDecimal.ZERO && missQty > calculatedIssueQty) { - println("⚠️ Warning: User reported missQty (${missQty}) exceeds calculated issueQty (${calculatedIssueQty})") - println(" BookQty: ${bookQty}, ActualPickQty: ${actualPickQty}") + println("⚠️ Warning: User reported missQty ($missQty) exceeds calculated issueQty ($calculatedIssueQty)") + println(" BookQty: $bookQty, ActualPickQty: $actualPickQty") } calculatedIssueQty } - - else -> BigDecimal.ZERO + else -> { + println(" Case 4: Default case") + println(" issueQty = 0") + BigDecimal.ZERO + } } - println("=== PICK EXECUTION ISSUE PROCESSING ===") - println("Required Qty: ${requiredQty}") - println("Actual Pick Qty: ${actualPickQty}") - println("Miss Qty (Reported): ${missQty}") - println("Bad Item Qty: ${badItemQty}") - println("Book Qty (inQty - outQty): ${bookQty}") - println("Issue Qty (Calculated): ${issueQty}") - println("Bad Reason: ${request.badReason}") - println("Lot ID: ${request.lotId}") - println("Item ID: ${request.itemId}") + println("=== Final IssueQty Calculation ===") + println(" Calculated IssueQty: $issueQty") println("================================================") // 5. 创建 pick execution issue 记录 + val issueNo = generateIssueNo() + println("Generated issue number: $issueNo") + val pickExecutionIssue = PickExecutionIssue( id = null, pickOrderId = request.pickOrderId, @@ -159,7 +198,7 @@ open class PickExecutionIssueService( pickOrderCreateDate = request.pickOrderCreateDate, pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(), pickOrderLineId = request.pickOrderLineId, - issueNo = generateIssueNo(), + issueNo = issueNo, joPickOrderId = pickOrder?.jobOrder?.id, doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null, issueCategory = IssueCategory.valueOf( @@ -189,11 +228,18 @@ open class PickExecutionIssueService( modifiedBy = "system", deleted = false ) - + + println("Creating pick execution issue record...") val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue) - + println("✅ Issue record created successfully!") + println(" Issue ID: ${savedIssue.id}") + println(" Issue No: ${savedIssue.issueNo}") + println(" Handle Status: ${savedIssue.handleStatus}") + println(" Issue Qty: ${savedIssue.issueQty}") + // 6. NEW: Update inventory_lot_line.issueQty if (request.lotId != null && inventoryLotLine != null) { + println("Updating inventory_lot_line.issueQty...") // ✅ 修改:如果只有 missQty,不更新 issueQty val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO val missQty = request.missQty ?: BigDecimal.ZERO @@ -205,6 +251,9 @@ open class PickExecutionIssueService( val hasMissItemWithPartialPick = missQty > BigDecimal.ZERO && actualPickQty > BigDecimal.ZERO + println(" isMissItemOnly: $isMissItemOnly") + println(" hasMissItemWithPartialPick: $hasMissItemWithPartialPick") + if (!isMissItemOnly && !hasMissItemWithPartialPick) { // 只有非 miss item 的情况才更新 issueQty val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO @@ -213,42 +262,45 @@ open class PickExecutionIssueService( inventoryLotLine.modified = LocalDateTime.now() inventoryLotLine.modifiedBy = "system" inventoryLotLineRepository.saveAndFlush(inventoryLotLine) - println("Updated inventory_lot_line ${request.lotId} issueQty: ${currentIssueQty} -> ${newIssueQty}") + println("✅ Updated inventory_lot_line ${request.lotId} issueQty: $currentIssueQty -> $newIssueQty") } else { - println("Skipped updating issueQty for miss item (lot ${request.lotId})") + println("⏭️ Skipped updating issueQty for miss item (lot ${request.lotId})") } } - // 7. 获取相关数据用于后续处理 val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO val lotId = request.lotId val itemId = request.itemId - - println("=== PICK EXECUTION ISSUE PROCESSING (NEW LOGIC) ===") - println("Actual Pick Qty: ${actualPickQtyForProcessing}") - println("Miss Qty: ${missQtyForProcessing}") - println("Bad Item Qty: ${badItemQtyForProcessing}") + + println("=== Processing Logic Selection ===") + println("Actual Pick Qty: $actualPickQtyForProcessing") + println("Miss Qty: $missQtyForProcessing") + println("Bad Item Qty: $badItemQtyForProcessing") println("Bad Reason: ${request.badReason}") - println("Lot ID: ${lotId}") - println("Item ID: ${itemId}") + println("Lot ID: $lotId") + println("Item ID: $itemId") println("================================================") - + // 8. 新的统一处理逻辑(根据 badReason 决定处理方式) when { // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> { + println("→ Handling: Miss Item Only") handleMissItemOnly(request, missQtyForProcessing) } // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { + println("→ Handling: Bad Item Only") // NEW: Check bad reason if (request.badReason == "package_problem") { + println(" Bad reason is 'package_problem' - calling handleBadItemPackageProblem") handleBadItemPackageProblem(request, badItemQtyForProcessing) } else { + println(" Bad reason is 'quantity_problem' - calling handleBadItemOnly") // quantity_problem or default: handle as normal bad item handleBadItemOnly(request, badItemQtyForProcessing) } @@ -256,29 +308,34 @@ open class PickExecutionIssueService( // 情况3: 既有 miss item 又有 bad item missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { + println("→ Handling: Both Miss and Bad Item") // NEW: Check bad reason if (request.badReason == "package_problem") { + println(" Bad reason is 'package_problem' - calling handleBothMissAndBadItemPackageProblem") handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing) } else { + println(" Bad reason is 'quantity_problem' - calling handleBothMissAndBadItem") handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing) } } // 情况4: 有 miss item 的情况(无论 actualPickQty 是多少) missQtyForProcessing > BigDecimal.ZERO -> { + println("→ Handling: Miss Item With Partial Pick") handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) } // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) actualPickQtyForProcessing > BigDecimal.ZERO -> { + println("→ Handling: Normal Pick") handleNormalPick(request, actualPickQtyForProcessing) } else -> { - println("Unknown case: actualPickQty=${actualPickQtyForProcessing}, missQty=${missQtyForProcessing}, badItemQty=${badItemQtyForProcessing}") + println("⚠️ Unknown case: actualPickQty=$actualPickQtyForProcessing, missQty=$missQtyForProcessing, badItemQty=$badItemQtyForProcessing") } } - + val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null) val consoCode = pickOrderForCompletion?.consoCode @@ -300,7 +357,9 @@ open class PickExecutionIssueService( println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}") } } - + + println("=== recordPickExecutionIssue: SUCCESS ===") + println("Issue ID: ${savedIssue.id}, Issue No: ${savedIssue.issueNo}") return MessageResponse( id = savedIssue.id, name = "Pick execution issue recorded successfully", @@ -309,9 +368,10 @@ open class PickExecutionIssueService( message = "Pick execution issue recorded successfully", errorPosition = null ) - + } catch (e: Exception) { - println("=== ERROR IN recordPickExecutionIssue ===") + println("=== recordPickExecutionIssue: ERROR ===") + println("Error: ${e.message}") e.printStackTrace() return MessageResponse( id = null, @@ -2585,10 +2645,11 @@ open fun getLotIssueDetails(lotId: Long, itemId: Long, issueType: String): LotIs ) } -// New: Submit with custom quantity -@Transactional(rollbackFor = [Exception::class]) open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse { try { + println("=== submitIssueWithQty: START ===") + println("Request: lotId=${request.lotId}, itemId=${request.itemId}, issueType=${request.issueType}, submitQty=${request.submitQty}, handler=${request.handler}") + // Find all issues for this lot and item val issues = if (request.issueType == "miss") { pickExecutionIssueRepository.findMissItemList(IssueCategory.lot_issue) @@ -2606,7 +2667,13 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse } } + println("Found ${issues.size} issues to process") + issues.forEachIndexed { index, issue -> + println(" Issue[$index]: id=${issue.id}, issueQty=${issue.issueQty}, handleStatus=${issue.handleStatus}") + } + if (issues.isEmpty()) { + println("❌ No issues found for this lot") return MessageResponse( id = null, name = "Error", @@ -2623,13 +2690,36 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse // Use custom quantity instead of sum val submitQty = request.submitQty - if (submitQty <= BigDecimal.ZERO) { + // ✅ 修改:允许提交0,0表示"标记为已处理但无需出库" + if (submitQty < BigDecimal.ZERO) { + println("❌ Submit quantity cannot be negative: $submitQty") return MessageResponse( id = null, name = "Error", code = "INVALID", type = "stock_issue", - message = "Submit quantity must be greater than 0", + message = "Submit quantity cannot be negative", + errorPosition = null + ) + } + + // ✅ 新增:如果提交数量为0,只标记issue为已处理,不创建stock_out_line + if (submitQty == BigDecimal.ZERO) { + println("ℹ️ Submit quantity is 0 - marking issues as handled without creating stock out") + + // Mark all issues as handled + issues.forEach { issue -> + println(" Marking issue ${issue.id} as handled by handler $handler") + markIssueHandled(issue, handler) + } + + println("✅ All issues marked as handled (no stock out created)") + return MessageResponse( + id = null, + name = "Success", + code = "SUCCESS", + type = "stock_issue", + message = "Issues marked as handled (no stock out - quantity is 0)", errorPosition = null ) } @@ -2649,14 +2739,17 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse firstIssue.issueRemark, handler ) + println("Created stock out header: id=${stockOut.id}, type=${if (isMissItem) "MISS_ITEM" else "BAD_ITEM"}") val pickOrderLine = firstIssue.pickOrderLineId?.let { pickOrderLineRepository.findById(it).orElse(null) } + println("Pick order line: id=${pickOrderLine?.id}") val lotLine = request.lotId.let { inventoryLotLineRepository.findById(it).orElse(null) } + println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}") val item = itemsRepository.findById(request.itemId).orElse(null) ?: return MessageResponse( @@ -2667,6 +2760,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse message = "Item not found", errorPosition = null ) + println("Item: id=${item.id}, code=${item.code}") // Create stock_out_line with custom quantity val stockOutLine = StockOutLine().apply { @@ -2679,21 +2773,27 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse this.type = if (isMissItem) "Miss" else "Bad" } val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + println("Created stock out line: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}") + if (!isMissItem && request.lotId != null) { val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null) if (lotLineForReset != null) { + val oldIssueQty = lotLineForReset.issueQty lotLineForReset.issueQty = BigDecimal.ZERO inventoryLotLineRepository.saveAndFlush(lotLineForReset) - println("✅ Reset issueQty to 0 for lot ${request.lotId} before bad item submission (submitIssueWithQty)") + println("✅ Reset issueQty for lot ${request.lotId}: $oldIssueQty -> 0") } } + // Update inventory_lot_line with custom quantity - pass isMissItem flag if (request.lotId != null) { + println("Updating lot line after issue: lotId=${request.lotId}, submitQty=$submitQty, isMissItem=$isMissItem") updateLotLineAfterIssue(request.lotId, submitQty, isMissItem) } // Mark all issues as handled issues.forEach { issue -> + println(" Marking issue ${issue.id} as handled by handler $handler") markIssueHandled(issue, handler) } @@ -2708,6 +2808,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad") + println("=== submitIssueWithQty: SUCCESS ===") return MessageResponse( id = stockOut.id, name = "Success", @@ -2717,6 +2818,9 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse errorPosition = null ) } catch (e: Exception) { + println("=== submitIssueWithQty: ERROR ===") + println("Error: ${e.message}") + e.printStackTrace() return MessageResponse( id = null, name = "Error", diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt index f39929f..deb791b 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt @@ -3,12 +3,31 @@ package com.ffii.fpsms.modules.productProcess.entity import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime + @Repository interface ProductProcessLineRepository : JpaRepository { fun findByProductProcess_Id(productProcessId: Long): List fun findByProductProcess_IdAndHandler_Id(productProcessId: Long, handlerId: Long): List fun findByHandler_IdAndStartTimeIsNotNullAndEndTimeIsNull(handlerId: Long): List fun findByProductProcess_IdIn(ids: List): List + @Query("SELECT l FROM ProductProcessLine l LEFT JOIN FETCH l.equipment WHERE l.productProcess.id = :productProcessId") -fun findByProductProcess_IdWithEquipment(productProcessId: Long): List + fun findByProductProcess_IdWithEquipment(productProcessId: Long): List + + // 用於 Operator KPI:抓取在指定時間範圍內有重疊的所有行 + @Query( + """ + SELECT l FROM ProductProcessLine l + WHERE l.deleted = false + AND l.startTime IS NOT NULL + AND ( + (l.startTime <= :endOfDay AND (l.endTime IS NULL OR l.endTime >= :startOfDay)) + ) + """ + ) + fun findAllOverlappingWithDateRange(startOfDay: LocalDateTime, endOfDay: LocalDateTime): List + + // 用於 Equipment 狀態:查詢指定 equipmentDetailId 目前仍未完成的所有行 + fun findByEquipmentDetailIdAndDeletedFalseAndEndTimeIsNull(equipmentDetailId: Long): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt index f43dfa5..c1a2429 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt @@ -97,12 +97,12 @@ data class jobOrderLineInfo( val type: String?, val reqQty: Double?, - val baseReqQty: Int?, + val baseReqQty: Long?, val stockQty: Int?, val stockReqQty: Double?, - val baseStockQty: Int?, + val baseStockQty: Long?, val reqUom: String?, val reqBaseUom: String?, diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt index 8719763..a3b241f 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt @@ -239,4 +239,19 @@ class ProductProcessController( fun deleteProductProcessLine(@PathVariable lineId: Long): MessageResponse { return productProcessService.deleteProductProcessLine(lineId) } + + // ===== Dashboards ===== + + @GetMapping("/Demo/OperatorKpi") + fun getOperatorKpi(@RequestParam(required = false) date: String?): List { + val parsedDate = date?.takeIf { it.isNotBlank() }?.let { + LocalDate.parse(it, DateTimeFormatter.ISO_DATE) + } + return productProcessService.getOperatorKpi(parsedDate) + } + + @GetMapping("/Demo/EquipmentStatus") + fun getEquipmentStatus(): List { + return productProcessService.getEquipmentStatusByType() + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt index c7df76b..cd1494f 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt @@ -239,4 +239,64 @@ data class JobProcessStatusResponse( val planEndTime: LocalDateTime?, val status: String, val processes: List +) + +// ===== Operator KPI Dashboard ===== + +data class OperatorKpiProcessInfo( + val jobOrderId: Long?, + val jobOrderCode: String?, + val productProcessId: Long?, + val productProcessLineId: Long?, + val processName: String?, + val equipmentName: String?, + val equipmentDetailName: String?, + val startTime: LocalDateTime?, + val endTime: LocalDateTime?, + val processingTime: Int?, + val itemCode: String?, + val itemName: String?, +) + +data class OperatorKpiResponse( + val operatorId: Long, + val operatorName: String?, + val staffNo: String?, + val totalProcessingMinutes: Long, + val totalJobOrderCount: Int, + val currentProcesses: List, +) + +// ===== Equipment Status Dashboard ===== + +data class EquipmentStatusProcessInfo( + val jobOrderId: Long?, + val jobOrderCode: String?, + val productProcessId: Long?, + val productProcessLineId: Long?, + val processName: String?, + val operatorName: String?, + val startTime: LocalDateTime?, + val processingTime: Int?, + +) + +data class EquipmentStatusPerDetail( + val equipmentDetailId: Long, + val equipmentDetailCode: String?, + val equipmentDetailName: String?, + val equipmentId: Long?, + val equipmentTypeName: String?, + val status: String, + val repairAndMaintenanceStatus: Boolean?, + val latestRepairAndMaintenanceDate: LocalDateTime?, + val lastRepairAndMaintenanceDate: LocalDateTime?, + val repairAndMaintenanceRemarks: String?, + val currentProcess: EquipmentStatusProcessInfo?, +) + +data class EquipmentStatusByTypeResponse( + val equipmentTypeId: Long, + val equipmentTypeName: String?, + val details: List, ) \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql b/src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql index 2716bde..aec7cc7 100644 --- a/src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql +++ b/src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql @@ -1,7 +1,9 @@ -- liquibase formatted sql -- changeset KelvinY:add_baseScore_to_bom +-- preconditions onFail:MARK_RAN +-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'fpsmsdb' AND TABLE_NAME = 'bom_material' AND COLUMN_NAME = 'stockQty'; ALTER TABLE `fpsmsdb`.`bom_material` ADD COLUMN `stockQty` DECIMAL(14, 2) NULL DEFAULT NULL AFTER `salesUnitCode`, ADD COLUMN `stockUnit` INTEGER NULL DEFAULT NULL AFTER `stockQty`, -ADD COLUMN `stockUnitName` VARCHAR(255) NULL DEFAULT NULL AFTER `stockUnit`; +ADD COLUMN `stockUnitName` VARCHAR(255) NULL DEFAULT NULL AFTER `stockUnit`; \ No newline at end of file