| @@ -1032,73 +1032,125 @@ open fun recordSecondScanIssue(request: SecondScanIssueRequest): MessageResponse | |||||
| ) | ) | ||||
| } | } | ||||
| // ✅ Get pick order line details with item information | |||||
| val pickOrderLineResults = jdbcDao.queryForList( | |||||
| // ✅ Get pick order and pick order line details with item and lot information | |||||
| val pickOrderDetails = jdbcDao.queryForList( | |||||
| """ | """ | ||||
| SELECT | SELECT | ||||
| po.code as pickOrderCode, | |||||
| DATE_FORMAT(po.created, '%Y-%m-%d') as pickOrderCreateDate, | |||||
| pol.id as pickOrderLineId, | pol.id as pickOrderLineId, | ||||
| i.code as itemCode, | i.code as itemCode, | ||||
| i.name as itemName, | i.name as itemName, | ||||
| pol.qty as requiredQty | |||||
| FROM fpsmsdb.pick_order_line pol | |||||
| pol.qty as requiredQty, | |||||
| u.name as pickerName | |||||
| FROM fpsmsdb.pick_order po | |||||
| JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id | |||||
| JOIN fpsmsdb.items i ON i.id = pol.itemId | JOIN fpsmsdb.items i ON i.id = pol.itemId | ||||
| WHERE pol.poId = :pickOrderId AND pol.itemId = :itemId AND pol.deleted = false | |||||
| LEFT JOIN fpsmsdb.user u ON u.id = :createdBy | |||||
| WHERE po.id = :pickOrderId | |||||
| AND pol.itemId = :itemId | |||||
| AND pol.deleted = false | |||||
| AND po.deleted = false | |||||
| """, | """, | ||||
| mapOf( | mapOf( | ||||
| "pickOrderId" to request.pickOrderId, | "pickOrderId" to request.pickOrderId, | ||||
| "itemId" to request.itemId | |||||
| "itemId" to request.itemId, | |||||
| "createdBy" to request.createdBy | |||||
| ) | ) | ||||
| ) | ) | ||||
| if (pickOrderLineResults.isEmpty()) { | |||||
| // ✅ 修复:使用正确的变量名 pickOrderDetails | |||||
| if (pickOrderDetails.isEmpty()) { | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, | id = null, | ||||
| name = null, | name = null, | ||||
| code = "ERROR", | code = "ERROR", | ||||
| type = "NOT_FOUND", | type = "NOT_FOUND", | ||||
| message = "Pick order line not found for pickOrderId: ${request.pickOrderId}, itemId: ${request.itemId}", | |||||
| message = "Pick order not found for pickOrderId: ${request.pickOrderId}, itemId: ${request.itemId}", | |||||
| errorPosition = null | errorPosition = null | ||||
| ) | ) | ||||
| } | } | ||||
| val lineData = pickOrderLineResults.first() | |||||
| val pickOrderLineId = (lineData["pickOrderLineId"] as Number).toLong() | |||||
| val itemCode = lineData["itemCode"] as String? | |||||
| val itemName = lineData["itemName"] as String? | |||||
| val requiredQty = lineData["requiredQty"]?.let { | |||||
| // ✅ 修复:使用正确的变量名 pickOrderDetails | |||||
| val orderData = pickOrderDetails.first() | |||||
| val pickOrderCode = orderData["pickOrderCode"] as String? | |||||
| val pickOrderCreateDate = orderData["pickOrderCreateDate"]?.let { | |||||
| LocalDate.parse(it.toString()) | |||||
| } | |||||
| val pickOrderLineId = (orderData["pickOrderLineId"] as Number).toLong() | |||||
| val itemCode = orderData["itemCode"] as String? | |||||
| val itemName = orderData["itemName"] as String? | |||||
| val pickerName = orderData["pickerName"] as String? | |||||
| val requiredQty = orderData["requiredQty"]?.let { | |||||
| BigDecimal(it.toString()) | BigDecimal(it.toString()) | ||||
| } | } | ||||
| // ✅ 获取 suggested lot 信息(用于 lot_id, lot_no, store_location) | |||||
| val lotResults = jdbcDao.queryForList( | |||||
| """ | |||||
| SELECT | |||||
| ill.id as lotId, | |||||
| il.lotNo as lotNo, | |||||
| w.name as storeLocation | |||||
| FROM fpsmsdb.suggested_pick_lot spl | |||||
| JOIN fpsmsdb.inventory_lot_line ill ON spl.suggestedLotLineId = ill.id | |||||
| JOIN fpsmsdb.inventory_lot il ON ill.inventoryLotId = il.id | |||||
| LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||||
| WHERE spl.pickOrderLineId = :pickOrderLineId | |||||
| AND spl.deleted = false | |||||
| AND ill.deleted = false | |||||
| LIMIT 1 | |||||
| """, | |||||
| mapOf("pickOrderLineId" to pickOrderLineId) | |||||
| ) | |||||
| val lotId = if (lotResults.isNotEmpty()) { | |||||
| (lotResults.first()["lotId"] as Number?)?.toLong() | |||||
| } else null | |||||
| val lotNo = if (lotResults.isNotEmpty()) { | |||||
| lotResults.first()["lotNo"] as String? | |||||
| } else null | |||||
| val storeLocation = if (lotResults.isNotEmpty()) { | |||||
| lotResults.first()["storeLocation"] as String? | |||||
| } else null | |||||
| val joPickOrderEntity = joPickOrder.get() | val joPickOrderEntity = joPickOrder.get() | ||||
| joPickOrderEntity.matchStatus = JoPickOrderStatus.completed | joPickOrderEntity.matchStatus = JoPickOrderStatus.completed | ||||
| joPickOrderEntity.matchQty = request.qty.toInt() | joPickOrderEntity.matchQty = request.qty.toInt() | ||||
| // ✅ 添加:如果 ticketCompleteTime 还没设置,现在设置 | |||||
| if (joPickOrderEntity.ticketCompleteTime == null) { | if (joPickOrderEntity.ticketCompleteTime == null) { | ||||
| joPickOrderEntity.ticketCompleteTime = LocalDateTime.now() | joPickOrderEntity.ticketCompleteTime = LocalDateTime.now() | ||||
| } | } | ||||
| joPickOrderRepository.save(joPickOrderEntity) | joPickOrderRepository.save(joPickOrderEntity) | ||||
| // ✅ 生成 issueNo | |||||
| val issueNo = generateIssueNoForJo() | |||||
| // ✅ Create pick execution issue with complete data | // ✅ Create pick execution issue with complete data | ||||
| val pickExecutionIssue = PickExecutionIssue( | val pickExecutionIssue = PickExecutionIssue( | ||||
| issueNo = issueNo, | |||||
| type = request.type ?: "match", | |||||
| pickOrderId = request.pickOrderId, | pickOrderId = request.pickOrderId, | ||||
| pickOrderCode = "JO-${request.pickOrderId}", | |||||
| pickOrderCode = pickOrderCode ?: "P-${request.pickOrderId}", | |||||
| pickOrderCreateDate = pickOrderCreateDate, // ✅ 现在已经定义了 | |||||
| pickExecutionDate = LocalDate.now(), | |||||
| pickOrderLineId = pickOrderLineId, | pickOrderLineId = pickOrderLineId, | ||||
| itemId = request.itemId, | itemId = request.itemId, | ||||
| itemCode = itemCode, // ✅ Include item code | |||||
| itemDescription = itemName, // ✅ Include item name | |||||
| lotId = null, | |||||
| lotNo = null, | |||||
| storeLocation = null, | |||||
| requiredQty = requiredQty, // ✅ Include required quantity | |||||
| actualPickQty = request.qty.toBigDecimal(), // ✅ Include actual pick quantity | |||||
| missQty = if (request.isMissing) request.qty.toBigDecimal() else BigDecimal.ZERO, | |||||
| badItemQty = if (request.isBad) request.qty.toBigDecimal() else BigDecimal.ZERO, | |||||
| itemCode = itemCode, | |||||
| itemDescription = itemName, | |||||
| lotId = lotId, | |||||
| lotNo = lotNo, | |||||
| storeLocation = storeLocation, | |||||
| requiredQty = requiredQty, | |||||
| actualPickQty = request.qty.toBigDecimal(), | |||||
| missQty = request.missQty.toBigDecimal(), // ✅ 使用实际的 missQty | |||||
| badItemQty = request.badItemQty.toBigDecimal(), // ✅ 使用实际的 badItemQty | |||||
| issueRemark = request.reason, | issueRemark = request.reason, | ||||
| pickerName = null, | |||||
| handleStatus = HandleStatus.jopending, | |||||
| pickerName = pickerName, | |||||
| handleStatus = HandleStatus.pending, | |||||
| handleDate = null, | handleDate = null, | ||||
| handledBy = null, | handledBy = null, | ||||
| created = LocalDateTime.now(), | created = LocalDateTime.now(), | ||||
| @@ -1118,19 +1170,41 @@ open fun recordSecondScanIssue(request: SecondScanIssueRequest): MessageResponse | |||||
| message = "Second scan issue recorded successfully", | message = "Second scan issue recorded successfully", | ||||
| errorPosition = null | errorPosition = null | ||||
| ) | ) | ||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| println("❌ Error recording second scan issue: ${e.message}") | |||||
| println("=== ERROR in recordSecondScanIssue ===") | |||||
| e.printStackTrace() | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, | id = null, | ||||
| name = null, | name = null, | ||||
| code = "ERROR", | code = "ERROR", | ||||
| type = "EXCEPTION", | type = "EXCEPTION", | ||||
| message = "Error recording second scan issue: ${e.message}", | |||||
| message = "Error: ${e.message}", | |||||
| errorPosition = null | errorPosition = null | ||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| private fun generateIssueNoForJo(): String { | |||||
| val now = LocalDateTime.now() | |||||
| val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM")) | |||||
| // 查询当月最新的 issueNo | |||||
| val latestIssueNo = pickExecutionIssueRepository.findLatestIssueNoByYearMonth(yearMonth) | |||||
| // 计算下一个序列号 | |||||
| val nextSequence = if (latestIssueNo != null) { | |||||
| val parts = latestIssueNo.split("-") | |||||
| if (parts.size == 3) { | |||||
| val currentSequence = parts[2].toIntOrNull() ?: 0 | |||||
| currentSequence + 1 | |||||
| } else { | |||||
| 1 | |||||
| } | |||||
| } else { | |||||
| 1 | |||||
| } | |||||
| return "SKO-${yearMonth}-${nextSequence.toString().padStart(3, '0')}" | |||||
| } | |||||
| open fun getCompletedJobOrderPickOrdersWithCompletedSecondScan(userId: Long): List<Map<String, Any?>> { | open fun getCompletedJobOrderPickOrdersWithCompletedSecondScan(userId: Long): List<Map<String, Any?>> { | ||||
| println("=== getCompletedJobOrderPickOrdersWithCompletedSecondScan ===") | println("=== getCompletedJobOrderPickOrdersWithCompletedSecondScan ===") | ||||
| println("userId: $userId") | println("userId: $userId") | ||||
| @@ -144,22 +144,26 @@ class JobOrderController( | |||||
| } | } | ||||
| @PostMapping("/second-scan-issue/{pickOrderId}/{itemId}") | @PostMapping("/second-scan-issue/{pickOrderId}/{itemId}") | ||||
| fun recordSecondScanIssue( | |||||
| @PathVariable pickOrderId: Long, | |||||
| @PathVariable itemId: Long, | |||||
| @RequestBody data: Map<String, Any> | |||||
| ): MessageResponse { | |||||
| val request = SecondScanIssueRequest( | |||||
| pickOrderId = pickOrderId, // ✅ Use path variable | |||||
| itemId = itemId, // ✅ Use path variable | |||||
| qty = (data["qty"] as Number).toDouble(), | |||||
| isMissing = data["isMissing"] as Boolean, | |||||
| isBad = data["isBad"] as Boolean, | |||||
| reason = data["reason"] as String, | |||||
| createdBy = (data["createdBy"] as Number).toLong() | |||||
| ) | |||||
| return joPickOrderService.recordSecondScanIssue(request) | |||||
| } | |||||
| fun recordSecondScanIssue( | |||||
| @PathVariable pickOrderId: Long, | |||||
| @PathVariable itemId: Long, | |||||
| @RequestBody data: Map<String, Any> | |||||
| ): MessageResponse { | |||||
| val request = SecondScanIssueRequest( | |||||
| pickOrderId = pickOrderId, // ✅ path 变量 | |||||
| itemId = itemId, // ✅ path 变量 | |||||
| qty = (data["qty"] as Number).toDouble(), | |||||
| // ✅ 新增:安全读取 missQty/badItemQty/type,默认 0/"match" | |||||
| missQty = (data["missQty"] as? Number)?.toDouble() ?: 0.0, | |||||
| badItemQty = (data["badItemQty"] as? Number)?.toDouble() ?: 0.0, | |||||
| isMissing = data["isMissing"] as? Boolean ?: false, | |||||
| isBad = data["isBad"] as? Boolean ?: false, | |||||
| reason = data["reason"] as? String ?: "", | |||||
| createdBy = (data["createdBy"] as Number).toLong(), | |||||
| type = (data["type"] as? String) ?: "match" | |||||
| ) | |||||
| return joPickOrderService.recordSecondScanIssue(request) | |||||
| } | |||||
| @GetMapping("/completed-job-order-pick-orders-with-completed-second-scan/{userId}") | @GetMapping("/completed-job-order-pick-orders-with-completed-second-scan/{userId}") | ||||
| fun getCompletedJobOrderPickOrdersWithCompletedSecondScan(@PathVariable userId: Long): List<Map<String, Any?>> { | fun getCompletedJobOrderPickOrdersWithCompletedSecondScan(@PathVariable userId: Long): List<Map<String, Any?>> { | ||||
| return joPickOrderService.getCompletedJobOrderPickOrdersWithCompletedSecondScan(userId) | return joPickOrderService.getCompletedJobOrderPickOrdersWithCompletedSecondScan(userId) | ||||
| @@ -3,9 +3,12 @@ package com.ffii.fpsms.modules.jobOrder.web.model | |||||
| data class SecondScanIssueRequest( | data class SecondScanIssueRequest( | ||||
| val pickOrderId: Long, | val pickOrderId: Long, | ||||
| val itemId: Long, | val itemId: Long, | ||||
| val qty: Double, | |||||
| val qty: Double, // 这是 actual pick qty (verified qty) | |||||
| val missQty: Double = 0.0, // ✅ 添加:单独的 miss qty | |||||
| val badItemQty: Double = 0.0, // ✅ 添加:单独的 bad item qty | |||||
| val isMissing: Boolean, | val isMissing: Boolean, | ||||
| val isBad: Boolean, | val isBad: Boolean, | ||||
| val reason: String, | val reason: String, | ||||
| val createdBy: Long | |||||
| val createdBy: Long, | |||||
| val type: String? = "match" | |||||
| ) | ) | ||||
| @@ -27,7 +27,12 @@ class PickExecutionIssue( | |||||
| @Column(name = "pick_order_line_id", nullable = false) | @Column(name = "pick_order_line_id", nullable = false) | ||||
| val pickOrderLineId: Long, | val pickOrderLineId: Long, | ||||
| @Column(name = "issue_no", length = 50) | |||||
| val issueNo: String? = null, | |||||
| // ✅ 新增字段:type | |||||
| @Column(name = "type", length = 50) | |||||
| val type: String? = null, | |||||
| @Column(name = "item_id", nullable = false) | @Column(name = "item_id", nullable = false) | ||||
| val itemId: Long, | val itemId: Long, | ||||
| @@ -104,6 +109,8 @@ class PickExecutionIssue( | |||||
| itemCode = null, | itemCode = null, | ||||
| itemDescription = null, | itemDescription = null, | ||||
| lotId = null, | lotId = null, | ||||
| issueNo = null, | |||||
| type = null, | |||||
| lotNo = null, | lotNo = null, | ||||
| storeLocation = null, | storeLocation = null, | ||||
| requiredQty = null, | requiredQty = null, | ||||
| @@ -125,10 +132,7 @@ class PickExecutionIssue( | |||||
| } | } | ||||
| enum class HandleStatus { | enum class HandleStatus { | ||||
| pending, // ✅ Change to lowercase to match database | |||||
| handled, // ✅ Change to lowercase to match database | |||||
| resolved, | |||||
| jopending, | |||||
| johandled, | |||||
| joresolved // ✅ Change to lowercase to match database | |||||
| pending, | |||||
| sort_and_repair, | |||||
| dispose | |||||
| } | } | ||||
| @@ -3,7 +3,8 @@ package com.ffii.fpsms.modules.pickOrder.entity | |||||
| import org.springframework.data.jpa.repository.JpaRepository | import org.springframework.data.jpa.repository.JpaRepository | ||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||
| import org.springframework.data.jpa.repository.Query | |||||
| import org.springframework.data.repository.query.Param | |||||
| @Repository | @Repository | ||||
| interface PickExecutionIssueRepository : JpaRepository<PickExecutionIssue, Long> { | interface PickExecutionIssueRepository : JpaRepository<PickExecutionIssue, Long> { | ||||
| fun findByPickOrderIdAndDeletedFalse(pickOrderId: Long): List<PickExecutionIssue> | fun findByPickOrderIdAndDeletedFalse(pickOrderId: Long): List<PickExecutionIssue> | ||||
| @@ -13,4 +14,13 @@ interface PickExecutionIssueRepository : JpaRepository<PickExecutionIssue, Long> | |||||
| pickOrderLineId: Long, | pickOrderLineId: Long, | ||||
| lotId: Long | lotId: Long | ||||
| ): List<PickExecutionIssue> | ): List<PickExecutionIssue> | ||||
| @Query(""" | |||||
| SELECT p.issueNo | |||||
| FROM PickExecutionIssue p | |||||
| WHERE p.issueNo LIKE CONCAT('SKO-', :yearMonth, '-%') | |||||
| AND p.deleted = false | |||||
| ORDER BY p.issueNo DESC | |||||
| LIMIT 1 | |||||
| """) | |||||
| fun findLatestIssueNoByYearMonth(@Param("yearMonth") yearMonth: String): String? | |||||
| } | } | ||||
| @@ -50,6 +50,8 @@ open class PickExecutionIssueService( | |||||
| // 2. 创建 pick execution issue 记录 | // 2. 创建 pick execution issue 记录 | ||||
| val pickExecutionIssue = PickExecutionIssue( | val pickExecutionIssue = PickExecutionIssue( | ||||
| issueNo = generateIssueNo(), | |||||
| type = request.type, | |||||
| pickOrderId = request.pickOrderId, | pickOrderId = request.pickOrderId, | ||||
| pickOrderCode = request.pickOrderCode, | pickOrderCode = request.pickOrderCode, | ||||
| pickOrderCreateDate = request.pickOrderCreateDate, | pickOrderCreateDate = request.pickOrderCreateDate, | ||||
| @@ -146,6 +148,31 @@ open class PickExecutionIssueService( | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| private fun generateIssueNo(): String { | |||||
| val now = LocalDateTime.now() | |||||
| val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM")) | |||||
| // 查询当月最新的 issueNo | |||||
| val latestIssueNo = pickExecutionIssueRepository.findLatestIssueNoByYearMonth(yearMonth) | |||||
| // 计算下一个序列号 | |||||
| val nextSequence = if (latestIssueNo != null) { | |||||
| // 从最新的 issueNo 中提取序列号 | |||||
| // 例如: SKO-2510-001 -> 001 | |||||
| val parts = latestIssueNo.split("-") | |||||
| if (parts.size == 3) { | |||||
| val currentSequence = parts[2].toIntOrNull() ?: 0 | |||||
| currentSequence + 1 | |||||
| } else { | |||||
| 1 | |||||
| } | |||||
| } else { | |||||
| 1 | |||||
| } | |||||
| // 格式化为 SKO-YYMM-001 | |||||
| return "SKO-${yearMonth}-${nextSequence.toString().padStart(3, '0')}" | |||||
| } | |||||
| // FPSMS-backend/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt | // FPSMS-backend/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt | ||||
| // ✅ 修复:处理有部分拣货但有 miss item 的情况 | // ✅ 修复:处理有部分拣货但有 miss item 的情况 | ||||
| @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class]) | @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class]) | ||||
| @@ -1843,57 +1843,93 @@ open fun autoAssignAndReleasePickOrderByStoreAndTicket(storeId: String, ticketNo | |||||
| val pickOrderIdsStr = pickOrderIds.joinToString(",") | val pickOrderIdsStr = pickOrderIds.joinToString(",") | ||||
| val sql = """ | val sql = """ | ||||
| SELECT | |||||
| -- Pick Order Information | |||||
| po.id as pickOrderId, | |||||
| po.code as pickOrderCode, | |||||
| po.consoCode as pickOrderConsoCode, -- ✅ 添加 consoCode | |||||
| po.targetDate as pickOrderTargetDate, | |||||
| po.type as pickOrderType, | |||||
| po.status as pickOrderStatus, | |||||
| po.assignTo as pickOrderAssignTo, | |||||
| -- Pick Order Line Information | |||||
| pol.id as pickOrderLineId, | |||||
| pol.qty as pickOrderLineRequiredQty, | |||||
| -- Item Information | |||||
| i.id as itemId, | |||||
| i.code as itemCode, | |||||
| i.name as itemName, | |||||
| uc.code as uomCode, | |||||
| uc.udfudesc as uomDesc, | |||||
| -- ✅ Calculate total picked quantity from stock_out_line table | |||||
| COALESCE(( | |||||
| SELECT SUM(sol_picked.qty) | |||||
| FROM fpsmsdb.stock_out_line sol_picked | |||||
| WHERE sol_picked.pickOrderLineId = pol.id | |||||
| AND sol_picked.deleted = false | |||||
| AND sol_picked.status IN ('completed', 'COMPLETE', 'partially_completed','rejected') | |||||
| ), 0) as totalPickedQty, | |||||
| -- ✅ Calculate available quantity from inventory | |||||
| COALESCE(( | |||||
| SELECT inv.onHandQty - inv.onHoldQty - inv.unavailableQty | |||||
| FROM fpsmsdb.inventory inv | |||||
| WHERE inv.itemId = i.id | |||||
| AND inv.deleted = false | |||||
| ), 0) as availableQty | |||||
| FROM fpsmsdb.pick_order po | |||||
| JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id | |||||
| JOIN fpsmsdb.items i ON i.id = pol.itemId | |||||
| LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId | |||||
| WHERE po.deleted = false | |||||
| AND po.id IN ($pickOrderIdsStr) | |||||
| AND pol.deleted = false | |||||
| AND po.status = 'RELEASED' | |||||
| SELECT | |||||
| -- Pick Order Information | |||||
| po.id as pickOrderId, | |||||
| po.code as pickOrderCode, | |||||
| po.consoCode as pickOrderConsoCode, | |||||
| po.targetDate as pickOrderTargetDate, | |||||
| po.type as pickOrderType, | |||||
| po.status as pickOrderStatus, | |||||
| po.assignTo as pickOrderAssignTo, | |||||
| ORDER BY | |||||
| po.code ASC, | |||||
| i.code ASC | |||||
| """.trimIndent() | |||||
| -- Pick Order Line Information | |||||
| pol.id as pickOrderLineId, | |||||
| pol.qty as pickOrderLineRequiredQty, | |||||
| pol.status as pickOrderLineStatus, | |||||
| -- Item Information | |||||
| i.id as itemId, | |||||
| i.code as itemCode, | |||||
| i.name as itemName, | |||||
| uc.code as uomCode, | |||||
| uc.udfudesc as uomDesc, | |||||
| -- ✅ Calculate total picked quantity from stock_out_line table | |||||
| COALESCE(( | |||||
| SELECT SUM(sol_picked.qty) | |||||
| FROM fpsmsdb.stock_out_line sol_picked | |||||
| WHERE sol_picked.pickOrderLineId = pol.id | |||||
| AND sol_picked.deleted = false | |||||
| AND sol_picked.status IN ('completed', 'COMPLETE', 'partially_completed','rejected') | |||||
| ), 0) as totalPickedQty, | |||||
| -- ✅ Calculate available quantity from inventory | |||||
| COALESCE(( | |||||
| SELECT inv.onHandQty - inv.onHoldQty - inv.unavailableQty | |||||
| FROM fpsmsdb.inventory inv | |||||
| WHERE inv.itemId = i.id | |||||
| AND inv.deleted = false | |||||
| ), 0) as availableQty, | |||||
| -- ✅ Check if all stock out lines for this pick order line are completed | |||||
| CASE | |||||
| WHEN EXISTS ( | |||||
| SELECT 1 | |||||
| FROM fpsmsdb.stock_out_line sol_check | |||||
| WHERE sol_check.pickOrderLineId = pol.id | |||||
| AND sol_check.deleted = false | |||||
| AND sol_check.status NOT IN ('completed', 'COMPLETE') | |||||
| ) THEN false | |||||
| WHEN EXISTS ( | |||||
| SELECT 1 | |||||
| FROM fpsmsdb.stock_out_line sol_check | |||||
| WHERE sol_check.pickOrderLineId = pol.id | |||||
| AND sol_check.deleted = false | |||||
| ) THEN true | |||||
| ELSE false | |||||
| END as allLotsCompleted | |||||
| FROM fpsmsdb.pick_order po | |||||
| JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id | |||||
| JOIN fpsmsdb.items i ON i.id = pol.itemId | |||||
| LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId | |||||
| WHERE po.deleted = false | |||||
| AND po.id IN ($pickOrderIdsStr) | |||||
| AND pol.deleted = false | |||||
| AND po.status = 'RELEASED' | |||||
| AND po.type NOT IN ('do', 'job') -- ✅ 排除 do 和 job 类型 | |||||
| -- ✅ Only include lines that have incomplete stock out lines | |||||
| AND ( | |||||
| NOT EXISTS ( | |||||
| SELECT 1 | |||||
| FROM fpsmsdb.stock_out_line sol_all_complete | |||||
| WHERE sol_all_complete.pickOrderLineId = pol.id | |||||
| AND sol_all_complete.deleted = false | |||||
| ) | |||||
| OR EXISTS ( | |||||
| SELECT 1 | |||||
| FROM fpsmsdb.stock_out_line sol_incomplete | |||||
| WHERE sol_incomplete.pickOrderLineId = pol.id | |||||
| AND sol_incomplete.deleted = false | |||||
| AND sol_incomplete.status NOT IN ('completed', 'COMPLETE') | |||||
| ) | |||||
| ) | |||||
| ORDER BY | |||||
| po.code ASC, | |||||
| i.code ASC | |||||
| """.trimIndent() | |||||
| println("🔍 Executing optimized SQL: $sql") | println("🔍 Executing optimized SQL: $sql") | ||||
| @@ -1966,6 +2002,7 @@ open fun autoAssignAndReleasePickOrderByStoreAndTicket(storeId: String, ticketNo | |||||
| val releasedPickOrderIds = allPickOrders | val releasedPickOrderIds = allPickOrders | ||||
| .filter { it.status == PickOrderStatus.RELEASED } | .filter { it.status == PickOrderStatus.RELEASED } | ||||
| .filter { it.assignTo?.id == userId } | .filter { it.assignTo?.id == userId } | ||||
| .filter { it.type?.value != "do" && it.type?.value != "jo" } // ✅ 排除 do 和 job 类型 | |||||
| .map { it.id!! } | .map { it.id!! } | ||||
| if (releasedPickOrderIds.isEmpty()) { | if (releasedPickOrderIds.isEmpty()) { | ||||
| @@ -15,6 +15,8 @@ data class PickExecutionIssueRequest( | |||||
| val itemDescription: String? = null, | val itemDescription: String? = null, | ||||
| val lotId: Long? = null, | val lotId: Long? = null, | ||||
| val lotNo: String? = null, | val lotNo: String? = null, | ||||
| val issueNo: String? = null, | |||||
| val type: String? = null, | |||||
| val storeLocation: String? = null, | val storeLocation: String? = null, | ||||
| val requiredQty: BigDecimal? = null, | val requiredQty: BigDecimal? = null, | ||||
| val actualPickQty: BigDecimal? = null, | val actualPickQty: BigDecimal? = null, | ||||
| @@ -0,0 +1,15 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset [enson]:update_pick_execution_issue_add_columns_and_modify_status | |||||
| -- 1. 添加新列 issueNo | |||||
| ALTER TABLE pick_execution_issue | |||||
| ADD COLUMN issue_no VARCHAR(50) NULL AFTER id; | |||||
| -- 2. 添加新列 Type | |||||
| ALTER TABLE pick_execution_issue | |||||
| ADD COLUMN type VARCHAR(50) NULL AFTER issue_no; | |||||
| -- 3. 修改 handle_status 枚举值 | |||||
| ALTER TABLE pick_execution_issue | |||||
| MODIFY COLUMN handle_status ENUM('pending', 'Sort and Repair', 'Dispose') NULL DEFAULT 'pending'; | |||||