From 99f7c59de1cf70c99007bb83961e9d52179b5bcd Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 17 Mar 2026 17:59:25 +0800 Subject: [PATCH] update --- .../jobOrder/service/JoPickOrderService.kt | 45 ++++++++++++ .../web/model/CreateJobOrderRequest.kt | 16 +++++ .../pickOrder/service/PickOrderService.kt | 6 +- .../entity/InventoryLotLineRepository.kt | 6 ++ .../stock/entity/SuggestPickLotRepository.kt | 2 + .../stock/service/StockOutLineService.kt | 70 +++++++++++++++---- .../stock/service/SuggestedPickLotService.kt | 34 ++++++++- .../stock/web/model/SaveStockOutRequest.kt | 1 + 8 files changed, 164 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt index 27f971b..5bbb869 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt @@ -45,6 +45,7 @@ import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoResponse import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderBasicInfoResponse import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderLineWithLotsResponse import com.ffii.fpsms.modules.jobOrder.web.model.LotDetailResponse +import com.ffii.fpsms.modules.jobOrder.web.model.StockOutLineDetailResponse import com.ffii.fpsms.modules.stock.entity.enum.StockInLineStatus import com.ffii.fpsms.modules.stock.entity.StockInLineRepository @@ -1960,6 +1961,23 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo val stockOutLinesByPickOrderLine = pickOrderLineIds.associateWith { polId -> stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(polId) } + + // stockouts 可能包含不在 suggestedPickLots 內的 inventoryLotLineId,需補齊以便計算 location/availableQty + val stockOutInventoryLotLineIds = stockOutLinesByPickOrderLine.values + .flatten() + .mapNotNull { it.inventoryLotLineId } + .distinct() + + val stockOutInventoryLotLines = if (stockOutInventoryLotLineIds.isNotEmpty()) { + inventoryLotLineRepository.findAllByIdIn(stockOutInventoryLotLineIds) + .filter { it.deleted == false } + } else { + emptyList() + } + + val inventoryLotLineById = (inventoryLotLines + stockOutInventoryLotLines) + .filter { it.id != null } + .associateBy { it.id!! } // 获取 stock in lines 通过 inventoryLotLineId(用于填充 stockInLineId) val stockInLinesByInventoryLotLineId = if (inventoryLotLineIds.isNotEmpty()) { @@ -2080,6 +2098,32 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo matchQty = jpo?.matchQty?.toDouble() ) } + + // 构建 stockouts 数据:用于无 suggested lot / noLot 场景也能显示并闭环(submit 0) + val stockouts = (stockOutLinesByPickOrderLine[lineId] ?: emptyList()).map { sol -> + val illId = sol.inventoryLotLineId + val ill = if (illId != null) inventoryLotLineById[illId] else null + val lot = ill?.inventoryLot + val warehouse = ill?.warehouse + val availableQty = if (sol.status == "rejected") { + null + } else if (ill == null || ill.deleted == true) { + null + } else { + (ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty ?: BigDecimal.ZERO) + } + + StockOutLineDetailResponse( + id = sol.id, + status = sol.status, + qty = sol.qty.toDouble(), + lotId = illId, + lotNo = sol.lotNo ?: lot?.lotNo, + location = warehouse?.code, + availableQty = availableQty?.toDouble(), + noLot = (illId == null) + ) + } PickOrderLineWithLotsResponse( id = pol.id!!, @@ -2091,6 +2135,7 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo uomDesc = uom?.udfudesc, status = pol.status?.value, lots = lots, + stockouts = stockouts, handler=handlerName ) } diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt index d2f4a42..87e58a9 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt @@ -96,9 +96,25 @@ data class PickOrderLineWithLotsResponse( val uomDesc: String?, val status: String?, val lots: List, + val stockouts: List = emptyList(), val handler: String? ) +/** + * Stock-out line rows that should be shown even when there is no suggested lot. + * `noLot=true` indicates this line currently has no lot assigned / insufficient inventory lot. + */ +data class StockOutLineDetailResponse( + val id: Long?, + val status: String?, + val qty: Double?, + val lotId: Long?, + val lotNo: String?, + val location: String?, + val availableQty: Double?, + val noLot: Boolean +) + data class LotDetailResponse( val lotId: Long?, val lotNo: String?, 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 3d995fb..b7c311c 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 @@ -4017,7 +4017,11 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto // Fallback lotNo (req.newInventoryLotNo is non-null String in your model) if (req.newInventoryLotNo.isNotBlank()) { - return inventoryLotLineRepository.findByLotNoAndItemId(req.newInventoryLotNo, itemId) + return inventoryLotLineRepository + .findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc( + req.newInventoryLotNo, + itemId + ) } return null diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt index b5c6711..69b48b9 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt @@ -52,6 +52,12 @@ interface InventoryLotLineRepository : AbstractRepository): List 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 8a60f8a..75983ab 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 @@ -11,4 +11,6 @@ interface SuggestPickLotRepository : AbstractRepository fun findFirstByPickOrderLineId(pickOrderLineId: Long): SuggestedPickLot? fun findAllByPickOrderLineId(pickOrderLineId: Long): List + + fun findFirstByStockOutLineId(stockOutLineId: Long): SuggestedPickLot? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 7e54be0..766f72c 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt @@ -43,6 +43,8 @@ import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository import com.ffii.fpsms.modules.stock.entity.InventoryRepository import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository import java.time.LocalTime +import com.ffii.fpsms.modules.stock.entity.StockInLineRepository +import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository @Service open class StockOutLineService( private val jdbcDao: JdbcDao, @@ -53,7 +55,9 @@ open class StockOutLineService( private val itemUomRespository: ItemUomRespository, private val pickOrderRepository: PickOrderRepository, private val inventoryLotLineRepository: InventoryLotLineRepository, + private val stockInLineRepository: StockInLineRepository, @Lazy private val suggestedPickLotService: SuggestedPickLotService, + private val suggestPickLotRepository: SuggestPickLotRepository, private val inventoryLotRepository: InventoryLotRepository, private val doPickOrderRepository: DoPickOrderRepository, private val doPickOrderRecordRepository: DoPickOrderRecordRepository, @@ -946,22 +950,62 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta // Step 2: Get InventoryLotLine val getInventoryLotLineStart = System.currentTimeMillis() - // 修复:从 stockOutLine.inventoryLotLine 获取 inventoryLot,而不是使用错误的参数 - val inventoryLotLine = stockOutLine.inventoryLotLine + // If StockOutLine has no lot (noLot row), resolve InventoryLotLine by scanned lotNo + itemId and bind it + var inventoryLotLine = stockOutLine.inventoryLotLine + if (inventoryLotLine == null) { + // Prefer stockInLineId from QR for deterministic binding + val resolved = if (request.stockInLineId != null && request.stockInLineId > 0) { + println(" Resolving InventoryLotLine by stockInLineId=${request.stockInLineId} ...") + val sil = stockInLineRepository.findById(request.stockInLineId).orElse(null) + val ill = sil?.inventoryLotLine + if (ill == null) { + println(" StockInLine ${request.stockInLineId} has no associated InventoryLotLine") + null + } else { + // item consistency guard + val illItemId = ill.inventoryLot?.item?.id + if (illItemId != null && illItemId != request.itemId) { + println(" InventoryLotLine item mismatch for stockInLineId=${request.stockInLineId}: $illItemId != ${request.itemId}") + null + } else { + ill + } + } + } else { + println(" StockOutLine has no associated InventoryLotLine, resolving by lotNo+itemId...") + inventoryLotLineRepository + .findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc( + request.inventoryLotNo, + request.itemId + ) + } + if (resolved == null) { + println(" Cannot resolve InventoryLotLine by lotNo=${request.inventoryLotNo}, itemId=${request.itemId}") + return MessageResponse( + id = null, + name = "No inventory lot line", + code = "NO_INVENTORY_LOT_LINE", + type = "error", + message = "Cannot resolve InventoryLotLine (stockInLineId=${request.stockInLineId ?: "null"}, lotNo=${request.inventoryLotNo}, itemId=${request.itemId})", + errorPosition = null + ) + } + // Bind the lot line to this stockOutLine so subsequent operations can proceed + stockOutLine.inventoryLotLine = resolved + stockOutLine.item = stockOutLine.item ?: resolved.inventoryLot?.item + inventoryLotLine = resolved + + // Also update SuggestedPickLot to point to the resolved lot line (so UI/holdQty logic matches DO confirmLotSubstitution) + val spl = suggestPickLotRepository.findFirstByStockOutLineId(stockOutLine.id!!) + if (spl != null) { + spl.suggestedLotLine = resolved + suggestPickLotRepository.saveAndFlush(spl) + } + } val getInventoryLotLineTime = System.currentTimeMillis() - getInventoryLotLineStart println("⏱️ [STEP 2] Get InventoryLotLine: ${getInventoryLotLineTime}ms") - if (inventoryLotLine == null) { - println(" StockOutLine has no associated InventoryLotLine") - return MessageResponse( - id = null, - name = "No inventory lot line", - code = "NO_INVENTORY_LOT_LINE", - type = "error", - message = "StockOutLine ${request.stockOutLineId} has no associated InventoryLotLine", - errorPosition = null - ) - } + // inventoryLotLine is guaranteed non-null here // Step 3: Get InventoryLot val getInventoryLotStart = System.currentTimeMillis() diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index 4c1656c..832e29a 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt @@ -40,6 +40,7 @@ import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository import com.ffii.fpsms.modules.stock.web.model.StockOutStatus +import com.ffii.fpsms.modules.common.SecurityUtils @Service open class SuggestedPickLotService( val suggestedPickLotRepository: SuggestPickLotRepository, @@ -433,7 +434,32 @@ open class SuggestedPickLotService( } open fun saveAll(request: List): List { - return suggestedPickLotRepository.saveAllAndFlush(request) + val saved = suggestedPickLotRepository.saveAllAndFlush(request) + + // For insufficient stock (suggestedLotLine == null), create a no-lot stock_out_line so UI can display & close the line. + // Also backfill SuggestedPickLot.stockOutLineId for downstream flows (e.g. hierarchical API -> stockouts). + val toBackfill = saved.filter { it.suggestedLotLine == null && it.pickOrderLine != null } + if (toBackfill.isNotEmpty()) { + val updated = mutableListOf() + toBackfill.forEach { spl -> + val pickOrder = spl.pickOrderLine?.pickOrder + if (pickOrder == null) return@forEach + + // Only create/backfill when stockOutLine is missing + if (spl.stockOutLine == null) { + val sol = createStockOutLineForSuggestion(spl, pickOrder) + if (sol != null) { + spl.stockOutLine = sol + updated.add(spl) + } + } + } + if (updated.isNotEmpty()) { + suggestedPickLotRepository.saveAllAndFlush(updated) + } + } + + return saved } private fun createStockOutLineForSuggestion( suggestion: SuggestedPickLot, @@ -470,10 +496,13 @@ open class SuggestedPickLotService( // Get or create StockOut val stockOut = stockOutRepository.findByConsoPickOrderCode(pickOrder.consoCode ?: "") .orElseGet { + val handlerId = pickOrder.assignTo?.id ?: SecurityUtils.getUser().orElse(null)?.id + require(handlerId != null) { "Cannot create StockOut: handlerId is null" } val newStockOut = StockOut().apply { this.consoPickOrderCode = pickOrder.consoCode ?: "" this.type = pickOrderTypeValue // Use pick order type (do, job, material, etc.) this.status = StockOutStatus.PENDING.status + this.handler = handlerId } stockOutRepository.save(newStockOut) } @@ -484,7 +513,8 @@ open class SuggestedPickLotService( this.pickOrderLine = pickOrderLine this.item = item this.inventoryLotLine = null // No lot available - this.qty = (suggestion.qty ?: BigDecimal.ZERO).toDouble() + // qty on StockOutLine represents picked qty; for no-lot placeholder it must start from 0 + this.qty = 0.0 this.status = StockOutLineStatus.PENDING.status this.deleted = false this.type = "Nor" diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt index 495237e..ee8fbaa 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt @@ -64,6 +64,7 @@ data class UpdateStockOutLineStatusRequest( data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( val pickOrderLineId: Long, val inventoryLotNo: String, + val stockInLineId: Long? = null, val stockOutLineId: Long, val itemId: Long, val status: String