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 e78d813..575affb 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 @@ -213,7 +213,8 @@ open class PickExecutionIssueService( println("=== Final IssueQty Calculation ===") println(" Calculated IssueQty: $issueQty") println("================================================") - + println("=== Processing Logic Selection ===") + // 5. 创建 pick execution issue 记录 val issueNo = generateIssueNo() println("Generated issue number: $issueNo") @@ -360,7 +361,12 @@ open class PickExecutionIssueService( println("→ Handling: Miss Item With Partial Pick") handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) } - + actualPickQtyForProcessing == BigDecimal.ZERO && + missQtyForProcessing == BigDecimal.ZERO && + badItemQtyForProcessing == BigDecimal.ZERO -> { + println("→ Handling: All zero, mark stock out line as completed") + handleAllZeroMarkCompleted(request) + } // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) actualPickQtyForProcessing > BigDecimal.ZERO -> { println("→ Handling: Normal Pick") @@ -419,6 +425,22 @@ open class PickExecutionIssueService( ) } } + private fun handleAllZeroMarkCompleted(request: PickExecutionIssueRequest) { + val stockOutLines = stockOutLineRepository + .findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( + request.pickOrderLineId, + request.lotId ?: 0L + ) + + stockOutLines.forEach { sol -> + sol.status = "completed" + sol.modified = LocalDateTime.now() + sol.modifiedBy = "system" + stockOutLineRepository.save(sol) + println("All-zero case: mark stock out line ${sol.id} as completed (qty kept as ${sol.qty})") + } + stockOutLineRepository.flush() + } private fun generateIssueNo(): String { val now = LocalDateTime.now() val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM")) diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt index 971f2fa..3d995fb 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt @@ -21,6 +21,7 @@ import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus import com.ffii.fpsms.modules.pickOrder.entity.PickOrderGroup import com.ffii.fpsms.modules.pickOrder.entity.PickOrderGroupRepository import com.ffii.fpsms.modules.pickOrder.web.models.* +import com.ffii.fpsms.modules.stock.entity.InventoryLotLine import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository import com.ffii.fpsms.modules.stock.entity.StockOut import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository @@ -3357,286 +3358,7 @@ ORDER BY val enrichedResults = filteredResults return enrichedResults } - // 修改后的逻辑 - /* - open fun getAllPickOrderLotsWithDetailsHierarchicalold(userId: Long): Map { - println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (Repository-based) ===") - println("userId filter: $userId") - - val user = userService.find(userId).orElse(null) - if (user == null) { - println("❌ User not found: $userId") - return emptyMap() - } - - // Step 1:直接按 handledBy 查当前用户的活动 do_pick_order(一个 ticket) - val activeTicketStatuses = listOf("released", "picking") // 如果你用的是 DoPickOrderStatus 枚举,也可以改成 List - val doPickOrder = doPickOrderRepository - .findFirstByHandledByAndDeletedFalseAndTicketStatusIn(user.id!!, activeTicketStatuses) - - if (doPickOrder == null) { - println("❌ No active do_pick_order found for handledBy user $userId") - return mapOf( - "fgInfo" to null, - "pickOrders" to emptyList() - ) - } - - val doPickOrderId = doPickOrder.id!! - println(" Using do_pick_order ID (by handledBy): $doPickOrderId") - - // Step 2:用这个 do_pick_orderId 查对应的 do_pick_order_line / pick_order - val allDoPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrderId) - val allPickOrderIdsForThisTicket = allDoPickOrderLines.mapNotNull { it.pickOrderId }.distinct() - - println(" Found ${allPickOrderIdsForThisTicket.size} pick orders in this do_pick_order (including completed)") - - // Step 3:加载这些 pick orders(包括 COMPLETED) - val pickOrders = pickOrderRepository.findAllById(allPickOrderIdsForThisTicket) - .filter { - it.deleted == false && - it.assignTo?.id == userId && - it.type?.value == "do" - } - - println(" Loaded ${pickOrders.size} pick orders (including completed)") - - // Step 4:原来你从 3413 行开始的收集所有 line / lots 的逻辑,全部保留 - val allPickOrderLineIds = pickOrders.flatMap { it.pickOrderLines }.mapNotNull { it.id } - - val allSuggestions = suggestPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIds) - val allStockOutLines = allPickOrderLineIds.flatMap { lineId -> - stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(lineId) - } - - val suggestionsByLineId = allSuggestions.groupBy { spl: SuggestedPickLot -> - spl.pickOrderLine?.id - } - val stockOutLinesByLineId = allStockOutLines.groupBy { sol: StockOutLineInfo -> - sol.pickOrderLineId - } - - val allPickOrderLines = mutableListOf>() - val lineCountsPerPickOrder = mutableListOf() - val pickOrderIdsList = mutableListOf() - val pickOrderCodesList = mutableListOf() - val doOrderIdsList = mutableListOf() - val deliveryOrderCodesList = mutableListOf() - - pickOrders.forEach { po -> - pickOrderIdsList.add(po.id!!) - pickOrderCodesList.add(po.code ?: "") - - val doOrderId = po.deliveryOrder?.id - if (doOrderId != null) doOrderIdsList.add(doOrderId) - deliveryOrderCodesList.add(po.deliveryOrder?.code ?: "") - - val lines = po.pickOrderLines.filter { !it.deleted } - - val lineDtos = po.pickOrderLines - .filter { !it.deleted } - .map { pol -> - val lineId = pol.id - val item = pol.item - val uom = pol.uom - - // 获取该 line 的 suggestions 和 stock out lines - val suggestions = lineId?.let { suggestionsByLineId[it] } ?: emptyList() - val stockOutLines = lineId?.let { stockOutLinesByLineId[it] } ?: emptyList() - - // 构建 lots(合并相同 lot 的多个 suggestions) - val lotMap = mutableMapOf() - - suggestions.forEach { spl -> - val ill = spl.suggestedLotLine - if (ill != null && ill.id != null) { - val illId = ill.id!! - val illEntity = inventoryLotLinesMap[illId] ?: ill - val il = illEntity.inventoryLot - val w = illEntity.warehouse - val isExpired = il?.expiryDate?.let { exp -> exp.isBefore(today) } == true - - val availableQty = (illEntity.inQty ?: zero) - .minus(illEntity.outQty ?: zero) - .minus(illEntity.holdQty ?: zero) - - // 查找对应的 stock out line - val stockOutLine = stockOutLines.find { sol -> - sol.inventoryLotLineId == illId - } - - // 计算 actualPickQty - val actualPickQty = stockOutLine?.qty?.let { numToBigDecimal(it as? Number) } - - if (lotMap.containsKey(illId)) { - // 合并 requiredQty - val existing = lotMap[illId]!! - val newRequiredQty = (existing.requiredQty ?: zero).plus(spl.qty ?: zero) - lotMap[illId] = existing.copy(requiredQty = newRequiredQty) - } else { - lotMap[illId] = LotDetailResponse( - id = illId, - lotNo = il?.lotNo, - expiryDate = il?.expiryDate, - location = w?.code, - stockUnit = illEntity.stockUom?.uom?.udfudesc ?: uom?.udfudesc ?: "N/A", - availableQty = availableQty, - requiredQty = spl.qty, - actualPickQty = actualPickQty, - inQty = illEntity.inQty, - outQty = illEntity.outQty, - holdQty = illEntity.holdQty, - lotStatus = illEntity.status?.value, - lotAvailability = when { - isExpired -> "expired" - stockOutLine?.status == "rejected" -> "rejected" - availableQty <= zero -> "insufficient_stock" - illEntity.status?.value == "unavailable" -> "status_unavailable" - else -> "available" - }, - processingStatus = when { - stockOutLine?.status == "completed" -> "completed" - stockOutLine?.status == "rejected" -> "rejected" - else -> "pending" - }, - suggestedPickLotId = spl.id, - stockOutLineId = stockOutLine?.id, - stockOutLineStatus = stockOutLine?.status, - stockOutLineQty = stockOutLine?.qty?.let { numToBigDecimal(it as? Number) }, - router = RouterInfoResponse( - id = null, - index = w?.order.toString(), - route = w?.code, - area = w?.code - ) - ) - } - } - } - - val lots = lotMap.values.toList() - - // 构建 stockouts(包括没有 lot 的) - val stockouts = stockOutLines.map { sol -> - val illId = sol.inventoryLotLineId - val ill = illId?.let { inventoryLotLinesMap[it] } - val il = ill?.inventoryLot - val w = ill?.warehouse - val available = if (ill == null) null else - (ill.inQty ?: zero) - .minus(ill.outQty ?: zero) - .minus(ill.holdQty ?: zero) - - StockOutDetailResponse( - id = sol.id, - status = sol.status, - qty = sol.qty?.let { numToBigDecimal(it as? Number) }, - lotId = ill?.id, - lotNo = il?.lotNo ?: "", - location = w?.code ?: "", - availableQty = available, - noLot = (ill == null) - ) - } - - PickOrderLineDetailResponse( - id = lineId, - requiredQty = pol.qty, - status = pol.status?.value, - item = ItemInfoResponse( - id = item?.id, - code = item?.code, - name = item?.name, - uomCode = uom?.code, - uomDesc = uom?.udfudesc, - uomShortDesc = uom?.udfShortDesc - ), - lots = lots, - stockouts = stockouts - ) - } - - - lineCountsPerPickOrder.add(lineDtos.size) - allPickOrderLines.addAll(lineDtos) - } - - // 排序、fgInfo、mergedPickOrder 这些也全部沿用你当前代码,只要用上面定义好的 doPickOrder/doPickOrderId 即可: - allPickOrderLines.sortWith(compareBy( - { line -> - val lots = line["lots"] as? List> - val firstLot = lots?.firstOrNull() - val router = firstLot?.get("router") as? Map - val indexValue = router?.get("index") - val floorSortValue = when (indexValue) { - is String -> { - val parts = indexValue.split("-") - if (parts.isNotEmpty()) { - val floorPart = parts[0].uppercase() - when (floorPart) { - "1F" -> 1 - "2F", "4F" -> 2 - else -> 3 - } - } else 3 - } - else -> 3 - } - floorSortValue - }, - { line -> - val lots = line["lots"] as? List> - val firstLot = lots?.firstOrNull() - val router = firstLot?.get("router") as? Map - val indexValue = router?.get("index") - when (indexValue) { - is Number -> indexValue.toInt() - is String -> { - val parts = indexValue.split("-") - if (parts.size > 1) { - parts.last().toIntOrNull() ?: 999999 - } else { - indexValue.toIntOrNull() ?: 999999 - } - } - else -> 999999 - } - } - )) - - val fgInfo = mapOf( - "doPickOrderId" to doPickOrderId, - "ticketNo" to doPickOrder.ticketNo, - "storeId" to doPickOrder.storeId, - "shopCode" to doPickOrder.shopCode, - "shopName" to doPickOrder.shopName, - "truckLanceCode" to doPickOrder.truckLanceCode, - "departureTime" to doPickOrder.truckDepartureTime?.toString() - ) - val mergedPickOrder = if (pickOrders.isNotEmpty()) { - val firstPickOrder = pickOrders.first() - mapOf( - "pickOrderIds" to pickOrderIdsList, - "pickOrderCodes" to pickOrderCodesList, - "doOrderIds" to doOrderIdsList, - "deliveryOrderCodes" to deliveryOrderCodesList, - "lineCountsPerPickOrder" to lineCountsPerPickOrder, - "consoCodes" to pickOrders.mapNotNull { it.consoCode }.distinct(), - "status" to doPickOrder.ticketStatus?.value, - "targetDate" to firstPickOrder.targetDate?.toLocalDate()?.toString(), - "pickOrderLines" to allPickOrderLines - ) - } else { - null - } - - return mapOf( - "fgInfo" to fgInfo, - "pickOrders" to listOfNotNull(mergedPickOrder) - ) -} -*/ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map { println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===") println("userId filter: $userId") @@ -4159,112 +3881,105 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto ) } } - @Transactional(rollbackFor = [java.lang.Exception::class]) open fun confirmLotSubstitution(req: LotSubstitutionConfirmRequest): MessageResponse { val zero = BigDecimal.ZERO // Validate pick order line - val pol = req.pickOrderLineId.let { pickOrderLineRepository.findById(it).orElse(null) } + val pol = pickOrderLineRepository.findById(req.pickOrderLineId).orElse(null) ?: return MessageResponse( id = null, name = "Pick order line not found", code = "ERROR", type = "pickorder", message = "Pick order line ${req.pickOrderLineId} not found", errorPosition = null ) val polItemId = pol.item?.id - if (polItemId == null) { - return MessageResponse( + ?: return MessageResponse( id = null, name = "Item not found", code = "ERROR", type = "pickorder", message = "Pick order line item is null", errorPosition = null ) - } - // ✅ 根据 lotNo 和 itemId 查找新的 InventoryLotLine - val newIll = when { - // 优先使用 stockInLineId(更可靠) - req.newStockInLineId != null && req.newStockInLineId > 0 -> { - // 通过 stockInLineId 查找 InventoryLot - val inventoryLot = inventoryLotRepository.findByStockInLineIdAndDeletedFalse(req.newStockInLineId) - ?: return MessageResponse( - id = null, name = "Inventory lot not found", code = "ERROR", type = "pickorder", - message = "Inventory lot with stockInLineId ${req.newStockInLineId} not found", - errorPosition = null - ) - - // 通过 InventoryLot 和 itemId 查找 InventoryLotLine - val lotLines = inventoryLotLineRepository.findAllByInventoryLotId(inventoryLot.id!!) - .filter { it.inventoryLot?.item?.id == polItemId && !it.deleted } - - if (lotLines.isEmpty()) { - return MessageResponse( - id = null, name = "Lot line not found", code = "ERROR", type = "pickorder", - message = "Inventory lot line with stockInLineId ${req.newStockInLineId} and itemId ${polItemId} not found", - errorPosition = null - ) - } - - // 如果有多个,取第一个(通常应该只有一个) - lotLines.first() - } - - // 兼容旧方式:使用 lotNo - req.newInventoryLotNo != null && req.newInventoryLotNo.isNotBlank() -> { - inventoryLotLineRepository.findByLotNoAndItemId(req.newInventoryLotNo, polItemId) - ?: return MessageResponse( - id = null, name = "New lot line not found", code = "ERROR", type = "pickorder", - message = "Inventory lot line with lotNo '${req.newInventoryLotNo}' and itemId ${polItemId} not found", - errorPosition = null - ) - } - - else -> { - return MessageResponse( - id = null, name = "Invalid request", code = "ERROR", type = "pickorder", - message = "Either newStockInLineId or newInventoryLotNo must be provided", - errorPosition = null - ) - } - } + // Find new InventoryLotLine (from stockInLineId first, fallback lotNo) + val newIll = resolveNewInventoryLotLine(req, polItemId) + ?: return MessageResponse( + id = null, name = "New lot line not found", code = "ERROR", type = "pickorder", + message = "Cannot resolve new inventory lot line", errorPosition = null + ) - // Item consistency check (应该已经通过上面的查询保证了,但再次确认) + // Item consistency check val newItemId = newIll.inventoryLot?.item?.id - if (newItemId == null || polItemId != newItemId) { + if (newItemId == null || newItemId != polItemId) { return MessageResponse( id = null, name = "Item mismatch", code = "ERROR", type = "pickorder", message = "New lot line item does not match pick order line item", errorPosition = null ) } - val newIllId = newIll.id ?: return MessageResponse( - id = null, name = "Invalid lot line", code = "ERROR", type = "pickorder", - message = "New inventory lot line has no ID", errorPosition = null - ) - val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo ?: "unknown" - // 1) Update suggested pick lot (if provided): move holdQty from old ILL to new ILL and re-point the suggestion - if (req.originalSuggestedPickLotId != null && req.originalSuggestedPickLotId > 0) { - // ✅ 使用 repository 而不是 SQL - val originalSpl = suggestPickLotRepository.findById(req.originalSuggestedPickLotId).orElse(null) - - if (originalSpl != null) { - val oldIll = originalSpl.suggestedLotLine - val qty = originalSpl.qty ?: zero + // Resolve SuggestedPickLot: + // - If originalSuggestedPickLotId provided: use it + // - Else (1:1 assumption): find by pickOrderLineId (optionally also by stockOutLineId if you add repository method) + val spl = if (req.originalSuggestedPickLotId != null && req.originalSuggestedPickLotId > 0) { + suggestPickLotRepository.findById(req.originalSuggestedPickLotId).orElse(null) + } else { + // 1:1 assumption fallback (you need a repository method; replace with your actual one) + // e.g. suggestPickLotRepository.findFirstByPickOrderLineIdAndDeletedFalseOrderByIdDesc(req.pickOrderLineId) + suggestPickLotRepository.findFirstByPickOrderLineId(req.pickOrderLineId) + } - if (oldIll != null && oldIll.id != newIllId) { - // Decrease hold on old, increase on new - oldIll.holdQty = (oldIll.holdQty ?: zero).minus(qty).max(zero) - inventoryLotLineRepository.save(oldIll) - - newIll.holdQty = (newIll.holdQty ?: zero).plus(qty) - inventoryLotLineRepository.save(newIll) - } + if (spl == null) { + return MessageResponse( + id = null, name = "Suggested pick lot not found", code = "ERROR", type = "pickorder", + message = "SuggestedPickLot not found for pickOrderLineId=${req.pickOrderLineId}", errorPosition = null + ) + } - // ✅ 使用 repository 更新 suggestion - originalSpl.suggestedLotLine = newIll - suggestPickLotRepository.save(originalSpl) - } + val qtyToHold = spl.qty ?: zero + if (qtyToHold.compareTo(zero) <= 0) { + return MessageResponse( + id = null, name = "Invalid qty", code = "ERROR", type = "pickorder", + message = "SuggestedPickLot qty is invalid: $qtyToHold", errorPosition = null + ) + } + + // Availability check on newIll BEFORE updates + val inQty = newIll.inQty ?: zero + val outQty = newIll.outQty ?: zero + val holdQty = newIll.holdQty ?: zero + val issueQty = newIll.issueQty ?: zero + val available = inQty.subtract(outQty).subtract(holdQty).subtract(issueQty) + + if (available.compareTo(qtyToHold) < 0) { + return MessageResponse( + id = null, name = "Insufficient lot qty", code = "REJECT", type = "pickorder", + message = "Reject switch lot: available=$available < required=$qtyToHold", errorPosition = null + ) } - // 2) Update stock out line (if provided): re-point to new ILL; keep qty and status unchanged + val oldIll = spl.suggestedLotLine + + // If oldIll exists and different: move hold old -> new + if (oldIll != null && oldIll.id != null && oldIll.id != newIll.id) { + val oldHold = oldIll.holdQty ?: zero + val newOldHold = oldHold.subtract(qtyToHold) + oldIll.holdQty = if (newOldHold.compareTo(zero) < 0) zero else newOldHold + inventoryLotLineRepository.save(oldIll) + + val newHold = (newIll.holdQty ?: zero).add(qtyToHold) + newIll.holdQty = newHold + inventoryLotLineRepository.save(newIll) + } + + // If first bind (oldIll == null): just hold on new + if (oldIll == null) { + val newHold = (newIll.holdQty ?: zero).add(qtyToHold) + newIll.holdQty = newHold + inventoryLotLineRepository.save(newIll) + } + + // Point suggestion to new lot line + spl.suggestedLotLine = newIll + suggestPickLotRepository.save(spl) + + // Update stock out line if provided if (req.stockOutLineId != null && req.stockOutLineId > 0) { val sol = stockOutLIneRepository.findById(req.stockOutLineId).orElse(null) if (sol != null) { @@ -4274,15 +3989,39 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto } } + val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo return MessageResponse( id = null, name = "Lot substitution confirmed", code = "SUCCESS", type = "pickorder", - message = "Updated suggestion and stock out line to new lot line with lotNo '${newLotNo}'", - errorPosition = null + message = "Updated suggestion and stock out line to new lot line with lotNo '$newLotNo'", + errorPosition = null ) } + + private fun resolveNewInventoryLotLine( + req: LotSubstitutionConfirmRequest, + itemId: Long + ): InventoryLotLine? { + // Prefer stockInLineId + if (req.newStockInLineId != null && req.newStockInLineId > 0) { + val inventoryLot = inventoryLotRepository.findByStockInLineIdAndDeletedFalse(req.newStockInLineId) + ?: return null + + val lotLines = inventoryLotLineRepository.findAllByInventoryLotId(inventoryLot.id!!) + .filter { it.inventoryLot?.item?.id == itemId && !it.deleted } + + return lotLines.firstOrNull() + } + + // Fallback lotNo (req.newInventoryLotNo is non-null String in your model) + if (req.newInventoryLotNo.isNotBlank()) { + return inventoryLotLineRepository.findByLotNoAndItemId(req.newInventoryLotNo, itemId) + } + + return null + } open fun getCompletedDoPickOrders( userId: Long, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt index f9b1e97..8a60f8a 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt @@ -8,7 +8,7 @@ import org.springframework.stereotype.Repository interface SuggestPickLotRepository : AbstractRepository { fun findAllByPickOrderLineIn(lines: List): List fun findAllByPickOrderLineIdIn(pickOrderLineIds: List): List - + fun findFirstByPickOrderLineId(pickOrderLineId: Long): SuggestedPickLot? fun findAllByPickOrderLineId(pickOrderLineId: Long): List } \ No newline at end of file