From f26a10348f8a8fcef00eb18436eb9f8f520b7a12 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 18 Mar 2026 01:14:56 +0800 Subject: [PATCH] update --- .../jobOrder/service/JoPickOrderService.kt | 80 ++++++++++++++++++- .../jobOrder/web/JobOrderController.kt | 9 ++- .../web/model/CreateJobOrderRequest.kt | 7 +- .../stock/service/StockInLineService.kt | 49 +++++++++--- 4 files changed, 129 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 5bbb869..cc49550 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 @@ -1758,7 +1758,7 @@ private fun normalizeFloor(raw: String): String { val num = cleaned.replace(Regex("[^0-9]"), "") return if (num.isNotEmpty()) "${num}F" else cleaned } -open fun getAllJoPickOrders(isDrink: Boolean?): List { +open fun getAllJoPickOrders(isDrink: Boolean?, floor: String?): List { println("=== getAllJoPickOrders ===") return try { @@ -1771,6 +1771,8 @@ open fun getAllJoPickOrders(isDrink: Boolean?): List { println("Found ${releasedPickOrders.size} released job order pick orders") val pickOrderIds = releasedPickOrders.mapNotNull { it.id } + val normalizedFloorFilter = floor?.let { normalizeFloor(it) }?.takeIf { it.isNotBlank() } + // 2. 批量查询每个 pick order 的按楼层统计(若没有则跳过) val floorCountsByPickOrderId: Map>> = if (pickOrderIds.isNotEmpty()) { val sql = """ @@ -1778,12 +1780,16 @@ open fun getAllJoPickOrders(isDrink: Boolean?): List { po.id AS pickOrderId, COALESCE(NULLIF(TRIM(w.store_id), ''), SUBSTRING_INDEX(w.code, '-', 1)) AS floorKey, COUNT(DISTINCT pol.id) AS totalCount, - COUNT(DISTINCT CASE WHEN pol.status = 'COMPLETED' THEN pol.id END) AS finishedCount + COUNT(DISTINCT CASE + WHEN LOWER(sol.status) = 'completed' THEN pol.id + WHEN pol.status = 'COMPLETED' THEN pol.id + END) AS finishedCount FROM fpsmsdb.pick_order po JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false LEFT JOIN fpsmsdb.suggested_pick_lot spl ON pol.id = spl.pickOrderLineId AND spl.deleted = false LEFT JOIN fpsmsdb.inventory_lot_line ill ON spl.suggestedLotLineId = ill.id AND ill.deleted = false LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId + LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false WHERE po.deleted = false AND po.status = 'RELEASED' AND po.joId IS NOT NULL @@ -1795,6 +1801,52 @@ open fun getAllJoPickOrders(isDrink: Boolean?): List { } else { emptyMap() } + + // 2b. no-lot counts (lines with stock_out_line.inventoryLotLineId IS NULL) + val noLotCountsByPickOrderId: Map> = if (pickOrderIds.isNotEmpty()) { + val sql = """ + SELECT + po.id AS pickOrderId, + COUNT(DISTINCT pol.id) AS totalCount, + COUNT(DISTINCT CASE + WHEN LOWER(sol.status) = 'completed' THEN pol.id + WHEN pol.status = 'COMPLETED' THEN pol.id + END) AS finishedCount + FROM fpsmsdb.pick_order po + JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false + JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false + WHERE po.deleted = false + AND po.status = 'RELEASED' + AND po.joId IS NOT NULL + AND po.id IN (${pickOrderIds.joinToString(",")}) + AND sol.inventoryLotLineId IS NULL + GROUP BY po.id + """.trimIndent() + jdbcDao.queryForList(sql, emptyMap()).associateBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L } + } else emptyMap() + + // 2c. suggested fail (rejected) counts per pick order + val suggestedFailCountByPickOrderId: Map = if (pickOrderIds.isNotEmpty()) { + val sql = """ + SELECT + po.id AS pickOrderId, + COUNT(DISTINCT sol.id) AS rejectedCount + FROM fpsmsdb.pick_order po + JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false + JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false + WHERE po.deleted = false + AND po.status = 'RELEASED' + AND po.joId IS NOT NULL + AND po.id IN (${pickOrderIds.joinToString(",")}) + AND LOWER(sol.status) = 'rejected' + GROUP BY po.id + """.trimIndent() + jdbcDao.queryForList(sql, emptyMap()).associate { row -> + val id = (row["pickOrderId"] as? Number)?.toLong() ?: 0L + val cnt = (row["rejectedCount"] as? Number)?.toInt() ?: 0 + id to cnt + } + } else emptyMap() val jobOrderPickOrders = releasedPickOrders.mapNotNull { pickOrder -> @@ -1844,6 +1896,26 @@ open fun getAllJoPickOrders(isDrink: Boolean?): List { totalCount = (row["totalCount"] as? Number)?.toInt() ?: 0 ) }.orEmpty() + + val noLotRow = noLotCountsByPickOrderId[pickOrder.id] + val noLotPickCount = if (noLotRow != null) { + com.ffii.fpsms.modules.jobOrder.web.model.FloorPickCountDto( + floor = "NO_LOT", + finishedCount = (noLotRow["finishedCount"] as? Number)?.toInt() ?: 0, + totalCount = (noLotRow["totalCount"] as? Number)?.toInt() ?: 0 + ) + } else null + + // Backend filtering by floor: include if there is remaining on that floor OR any no-lot remaining + if (normalizedFloorFilter != null) { + val hasRemainingOnFloor = floorPickCounts.any { c -> + c.floor == normalizedFloorFilter && (c.totalCount - c.finishedCount) > 0 + } + val hasNoLotRemaining = (noLotPickCount?.let { it.totalCount - it.finishedCount } ?: 0) > 0 + if (!hasRemainingOnFloor && !hasNoLotRemaining) return@mapNotNull null + } + + val suggestedFailCount = suggestedFailCountByPickOrderId[pickOrder.id] ?: 0 AllJoPickOrderResponse( id = pickOrder.id ?: 0L, pickOrderId = pickOrder.id, @@ -1861,7 +1933,9 @@ open fun getAllJoPickOrders(isDrink: Boolean?): List { lotNo = lotNo, jobOrderStatus = jobOrder.status?.value ?: "", finishedPickOLineCount = finishedLines, - floorPickCounts = floorPickCounts + floorPickCounts = floorPickCounts, + noLotPickCount = noLotPickCount, + suggestedFailCount = suggestedFailCount ) } diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt index a1c8fd5..106c668 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt @@ -243,8 +243,13 @@ fun recordSecondScanIssue( return joPickOrderService.getCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId) } @GetMapping("/AllJoPickOrder") - fun getAllJoPickOrder(@RequestParam(required = false) isDrink: Boolean?): List { - return joPickOrderService.getAllJoPickOrders(isDrink) + fun getAllJoPickOrder( + @RequestParam(required = false) isDrink: Boolean?, + // Single floor, e.g. "2F"/"3F"/"4F". When provided, backend returns job pick orders + // that still have unpicked lines on that floor OR any "no lot" remaining lines. + @RequestParam(required = false) floor: String? + ): List { + return joPickOrderService.getAllJoPickOrders(isDrink, floor) } @GetMapping("/all-lots-hierarchical-by-pick-order/{pickOrderId}") 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 87e58a9..7cb0a99 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 @@ -55,7 +55,12 @@ data class AllJoPickOrderResponse( val lotNo: String?, val jobOrderStatus: String, val finishedPickOLineCount: Int, - val floorPickCounts: List = emptyList() + val floorPickCounts: List = emptyList(), + // Lines which currently have no lot assigned (e.g. stockOutLine.inventoryLotLineId is null). + // For floor filtering, "no lot" should be treated as applicable to all floors. + val noLotPickCount: FloorPickCountDto? = null, + // Count of rejected stock-out lines for this pick order (proxy for "suggested fail" items). + val suggestedFailCount: Int = 0 ) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index d723695..570f0ed 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -129,23 +129,47 @@ open class StockInLineService( */ fun assignLotNo(): String { val prefix = "LT" - // 建議統一用同一種日期格式,現在 StockInLineService 是 DEFAULT_FORMATTER (LocalDate.today) val midfix = LocalDate.now().format(CodeGenerator.DEFAULT_FORMATTER) val fullPrefix = "$prefix-$midfix" - - // 1) 今天在 inventory_lot 裡的最大 lotNo + val latestFromInventory = inventoryLotRepository.findLatestLotNoByPrefix(fullPrefix) - - // 2) 今天在 stock_in_line 裡的最大 lotNo(不分來源:JO、PO、盤點...) + val latestFromStockInLine = stockInLineRepository.findLatestLotNoByPrefix(fullPrefix) - - // 3) 兩邊取最大的一個當成 latestCode + val latestCode = listOfNotNull(latestFromInventory, latestFromStockInLine) .maxOrNull() - - // 4) 丟給你現有的 CodeGenerator 產下一號 + + return CodeGenerator.generateNo( + prefix = prefix, + midfix = midfix, + latestCode = latestCode, + ) + } + + /** + * 專門給 JO 用的批號: + * - 日期部分用 job_order.planStart (若為 null 則退回今天) + * - 年份使用兩位數(例如 2026-03-18 -> 260318) + * - 序號仍然與現有規則一致,且跨 inventory_lot / stock_in_line 同步遞增 + */ + fun assignLotNoForJo(planStart: LocalDate?): String { + val prefix = "LT" + val date = planStart ?: LocalDate.now() + // 兩位數年份 + MMdd,例如 2026-03-18 -> "260318" + val midfix = date.format(DateTimeFormatter.ofPattern("yyMMdd")) + val fullPrefix = "$prefix-$midfix" + + val latestFromInventory = + inventoryLotRepository.findLatestLotNoByPrefix(fullPrefix) + + val latestFromStockInLine = + stockInLineRepository.findLatestLotNoByPrefix(fullPrefix) + + val latestCode = listOfNotNull(latestFromInventory, latestFromStockInLine) + .maxOrNull() + return CodeGenerator.generateNo( prefix = prefix, midfix = midfix, @@ -244,7 +268,8 @@ open class StockInLineService( status = StockInLineStatus.PENDING.status } if (jo != null) { - stockInLine.lotNo = assignLotNo() + val planStartDate = jo.planStart?.toLocalDate() + stockInLine.lotNo = assignLotNoForJo(planStartDate) } val savedSIL = saveAndFlush(stockInLine) if (pol != null) { @@ -680,6 +705,10 @@ open class StockInLineService( } else { requestQty ?: this.acceptedQty } + } else if (request.qcAccept == true && this.jobOrder != null) { + // For Job Order QC, allow updating received qty (acceptedQty) based on QC accept quantity. + // This enables stocking-in more than demand/previous received qty for JO flows only. + this.acceptedQty = request.acceptedQty } else if (request.qcAccept == true && this.status == StockInLineStatus.ESCALATED.status) { // Case: line was escalated (QC decision 3), handler resolves with decision 1 (accept). // Use 揀收數量 (acceptQty) for put away instead of keeping original received qty.