| @@ -1624,16 +1624,17 @@ open class PickOrderService( | |||||
| } | } | ||||
| throw IllegalStateException("Failed to generate unique delivery note code after $maxAttempts attempts") | throw IllegalStateException("Failed to generate unique delivery note code after $maxAttempts attempts") | ||||
| } | } | ||||
| /* | |||||
| @Transactional(rollbackFor = [java.lang.Exception::class]) | @Transactional(rollbackFor = [java.lang.Exception::class]) | ||||
| open fun checkAndCompletePickOrderByConsoCode(consoCode: String): MessageResponse { | open fun checkAndCompletePickOrderByConsoCode(consoCode: String): MessageResponse { | ||||
| try { | try { | ||||
| println("=== DEBUG: checkAndCompletePickOrderByConsoCode ===") | |||||
| println("consoCode: $consoCode") | |||||
| // println("=== DEBUG: checkAndCompletePickOrderByConsoCode ===") | |||||
| // println("consoCode: $consoCode") | |||||
| val stockOut = stockOutRepository.findFirstByConsoPickOrderCodeOrderByIdDesc(consoCode) | val stockOut = stockOutRepository.findFirstByConsoPickOrderCodeOrderByIdDesc(consoCode) | ||||
| if (stockOut == null) { | if (stockOut == null) { | ||||
| println("❌ No stock_out found for consoCode: $consoCode") | |||||
| //println("❌ No stock_out found for consoCode: $consoCode") | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, | id = null, | ||||
| name = "Stock out not found", | name = "Stock out not found", | ||||
| @@ -1680,13 +1681,13 @@ open class PickOrderService( | |||||
| !(isComplete || isRejected || isPartiallyComplete) | !(isComplete || isRejected || isPartiallyComplete) | ||||
| } | } | ||||
| println("📊 Stock out lines: ${stockOutLines.size}, Unfinished: ${unfinishedLines.size}") | |||||
| // println("📊 Stock out lines: ${stockOutLines.size}, Unfinished: ${unfinishedLines.size}") | |||||
| if (unfinishedLines.isEmpty()) { | if (unfinishedLines.isEmpty()) { | ||||
| println(" All stock out lines completed, updating pick order statuses...") | println(" All stock out lines completed, updating pick order statuses...") | ||||
| return completeStockOut(consoCode) | return completeStockOut(consoCode) | ||||
| } else { | } else { | ||||
| println("⏳ Still have ${unfinishedLines.size} unfinished lines") | |||||
| //println("⏳ Still have ${unfinishedLines.size} unfinished lines") | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = stockOut.id, | id = stockOut.id, | ||||
| name = stockOut.consoPickOrderCode ?: stockOut.deliveryOrderCode, | name = stockOut.consoPickOrderCode ?: stockOut.deliveryOrderCode, | ||||
| @@ -1710,8 +1711,47 @@ open class PickOrderService( | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| */ | |||||
| @Transactional(readOnly = true) | |||||
| open fun countUnfinishedLinesByConsoCode(consoCode: String): Int { | |||||
| val sql = """ | |||||
| SELECT COUNT(1) AS unfinished_count | |||||
| FROM stock_out_line sol | |||||
| JOIN pick_order_line pol ON pol.id = sol.pickOrderLineId | |||||
| JOIN pick_order po ON po.id = pol.poId | |||||
| WHERE po.consoCode = :consoCode | |||||
| AND sol.deleted = false | |||||
| AND LOWER(TRIM(COALESCE(sol.status, ''))) NOT IN ( | |||||
| 'completed', | |||||
| 'complete', | |||||
| 'rejected', | |||||
| 'partially_completed', | |||||
| 'partially_complete' | |||||
| ) | |||||
| """.trimIndent() | |||||
| val rows = jdbcDao.queryForList(sql, mapOf("consoCode" to consoCode)) | |||||
| val countAny = rows.firstOrNull()?.get("unfinished_count") | |||||
| return when (countAny) { | |||||
| is Number -> countAny.toInt() | |||||
| is String -> countAny.toIntOrNull() ?: 0 | |||||
| else -> 0 | |||||
| } | |||||
| } | |||||
| @Transactional(rollbackFor = [Exception::class]) | |||||
| open fun checkAndCompletePickOrderByConsoCode(consoCode: String): MessageResponse { | |||||
| val unfinished = countUnfinishedLinesByConsoCode(consoCode) | |||||
| if (unfinished > 0) { | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = consoCode, | |||||
| code = "NOT_COMPLETED", | |||||
| type = "pickorder", | |||||
| message = "Pick order not completed yet, $unfinished lines remaining", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| return completeStockOut(consoCode) | |||||
| } | |||||
| open fun createGroup(name: String, targetDate: LocalDate, pickOrderId: Long?): PickOrderGroup { | open fun createGroup(name: String, targetDate: LocalDate, pickOrderId: Long?): PickOrderGroup { | ||||
| val group = PickOrderGroup().apply { | val group = PickOrderGroup().apply { | ||||
| this.name = name | this.name = name | ||||
| @@ -305,6 +305,8 @@ ORDER BY | |||||
| fun searchStockTakeVarianceReportV2( | fun searchStockTakeVarianceReportV2( | ||||
| stockTakeRoundId: Long, | stockTakeRoundId: Long, | ||||
| itemCode: String?, | itemCode: String?, | ||||
| storeId: String?, | |||||
| status: String?, | |||||
| ): List<Map<String, Any>> { | ): List<Map<String, Any>> { | ||||
| val countSql = """ | val countSql = """ | ||||
| SELECT COUNT(*) AS c FROM stocktakerecord s | SELECT COUNT(*) AS c FROM stocktakerecord s | ||||
| @@ -320,6 +322,34 @@ ORDER BY | |||||
| val args = mutableMapOf<String, Any>() | val args = mutableMapOf<String, Any>() | ||||
| args["stockTakeRoundId"] = stockTakeRoundId | args["stockTakeRoundId"] = stockTakeRoundId | ||||
| val statusNormalized = status?.trim()?.lowercase().orEmpty() | |||||
| // status 映射规则: | |||||
| // - All/null:不过滤 | |||||
| // - pending:包含 pending/pass/notMatch | |||||
| // - completed:只看 completed | |||||
| val statusLatestSql = when (statusNormalized) { | |||||
| "pending" -> """ | |||||
| AND str.status IN ('pending', 'pass', 'notMatch') | |||||
| """.trimIndent() | |||||
| "completed" -> """ | |||||
| AND str.status = 'completed' | |||||
| """.trimIndent() | |||||
| else -> "" | |||||
| } | |||||
| val storeIdSql = run { | |||||
| val normalized = storeId?.trim() | |||||
| if (normalized.isNullOrBlank() || normalized.equals("all", ignoreCase = true)) { | |||||
| "" | |||||
| } else { | |||||
| args["storeId"] = normalized | |||||
| // DB 里 store_id 可能是 "2/F" 或 "2F";用 REPLACE 去斜線做匹配 | |||||
| """ | |||||
| AND REPLACE(COALESCE(wh.store_id, ''), '/', '') = REPLACE(:storeId, '/', '') | |||||
| """.trimIndent() | |||||
| } | |||||
| } | |||||
| val itemCodeSql = buildMultiValueLikeClause( | val itemCodeSql = buildMultiValueLikeClause( | ||||
| itemCode, | itemCode, | ||||
| "it.code", | "it.code", | ||||
| @@ -345,10 +375,12 @@ latest_str AS ( | |||||
| str.approverStockTakeQty, | str.approverStockTakeQty, | ||||
| str.date AS strDate, | str.date AS strDate, | ||||
| str.id, | str.id, | ||||
| str.approverTime | |||||
| str.approverTime, | |||||
| str.status AS stockTakeRecordStatus | |||||
| FROM stocktakerecord str | FROM stocktakerecord str | ||||
| WHERE str.deleted = 0 | WHERE str.deleted = 0 | ||||
| AND str.stockTakeRoundId = :stockTakeRoundId | AND str.stockTakeRoundId = :stockTakeRoundId | ||||
| $statusLatestSql | |||||
| ), | ), | ||||
| in_agg AS ( | in_agg AS ( | ||||
| SELECT | SELECT | ||||
| @@ -443,7 +475,8 @@ data AS ( | |||||
| ls.approverStockTakeQty AS stkApproverQty, | ls.approverStockTakeQty AS stkApproverQty, | ||||
| ls.varianceQty AS stkVarianceQty, | ls.varianceQty AS stkVarianceQty, | ||||
| ls.strDate AS stockTakeDateRaw, | ls.strDate AS stockTakeDateRaw, | ||||
| ls.approverTime AS approvalDateTimeRaw | |||||
| ls.approverTime AS approvalDateTimeRaw, | |||||
| ls.stockTakeRecordStatus AS stockTakeRecordStatus | |||||
| FROM latest_str ls | FROM latest_str ls | ||||
| INNER JOIN inventory_lot il | INNER JOIN inventory_lot il | ||||
| ON ls.lotId = il.id | ON ls.lotId = il.id | ||||
| @@ -471,6 +504,7 @@ data AS ( | |||||
| WHERE 1=1 | WHERE 1=1 | ||||
| $itemCodeSql | $itemCodeSql | ||||
| $storeIdSql | |||||
| ) | ) | ||||
| SELECT | SELECT | ||||
| @@ -501,12 +535,14 @@ SELECT | |||||
| END AS stockTakeQty, | END AS stockTakeQty, | ||||
| CASE | CASE | ||||
| WHEN COALESCE(stockTakeRecordStatus, '') <> 'completed' THEN '0' | |||||
| WHEN stkVarianceQty IS NULL THEN '0' | WHEN stkVarianceQty IS NULL THEN '0' | ||||
| WHEN COALESCE(stkVarianceQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkVarianceQty, 0), ')') | WHEN COALESCE(stkVarianceQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkVarianceQty, 0), ')') | ||||
| ELSE FORMAT(COALESCE(stkVarianceQty, 0), 0) | ELSE FORMAT(COALESCE(stkVarianceQty, 0), 0) | ||||
| END AS variance, | END AS variance, | ||||
| CASE | CASE | ||||
| WHEN COALESCE(stockTakeRecordStatus, '') <> 'completed' THEN '0%' | |||||
| WHEN stkVarianceQty IS NULL THEN '0%' | WHEN stkVarianceQty IS NULL THEN '0%' | ||||
| WHEN COALESCE(stkBookQty, 0) = 0 THEN '0%' | WHEN COALESCE(stkBookQty, 0) = 0 THEN '0%' | ||||
| WHEN (COALESCE(stkVarianceQty, 0) / stkBookQty) * 100 < 0 THEN CONCAT('(', FORMAT(-(COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%)') | WHEN (COALESCE(stkVarianceQty, 0) / stkBookQty) * 100 < 0 THEN CONCAT('(', FORMAT(-(COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%)') | ||||
| @@ -143,6 +143,8 @@ class StockTakeVarianceReportController( | |||||
| fun generateStockTakeVarianceReportV2( | fun generateStockTakeVarianceReportV2( | ||||
| @RequestParam stockTakeRoundId: Long, | @RequestParam stockTakeRoundId: Long, | ||||
| @RequestParam(required = false) itemCode: String?, | @RequestParam(required = false) itemCode: String?, | ||||
| @RequestParam(required = false, name = "store_id") storeId: String?, | |||||
| @RequestParam(required = false) status: String?, | |||||
| ): ResponseEntity<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val parameters = mutableMapOf<String, Any>() | val parameters = mutableMapOf<String, Any>() | ||||
| @@ -169,6 +171,8 @@ class StockTakeVarianceReportController( | |||||
| val dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2( | val dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2( | ||||
| stockTakeRoundId = stockTakeRoundId, | stockTakeRoundId = stockTakeRoundId, | ||||
| itemCode = itemCode, | itemCode = itemCode, | ||||
| storeId = storeId, | |||||
| status = status, | |||||
| ) | ) | ||||
| val stockTakeDateDisplay = dbData | val stockTakeDateDisplay = dbData | ||||
| .mapNotNull { it["stockTakeDate"] as? String } | .mapNotNull { it["stockTakeDate"] as? String } | ||||
| @@ -196,11 +200,15 @@ class StockTakeVarianceReportController( | |||||
| fun exportStockTakeVarianceReportV2Excel( | fun exportStockTakeVarianceReportV2Excel( | ||||
| @RequestParam stockTakeRoundId: Long, | @RequestParam stockTakeRoundId: Long, | ||||
| @RequestParam(required = false) itemCode: String?, | @RequestParam(required = false) itemCode: String?, | ||||
| @RequestParam(required = false, name = "store_id") storeId: String?, | |||||
| @RequestParam(required = false) status: String?, | |||||
| ): ResponseEntity<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| val cap = stockTakeVarianceReportService.getStockTakeRoundCaption(stockTakeRoundId) | val cap = stockTakeVarianceReportService.getStockTakeRoundCaption(stockTakeRoundId) | ||||
| val dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2( | val dbData = stockTakeVarianceReportService.searchStockTakeVarianceReportV2( | ||||
| stockTakeRoundId = stockTakeRoundId, | stockTakeRoundId = stockTakeRoundId, | ||||
| itemCode = itemCode, | itemCode = itemCode, | ||||
| storeId = storeId, | |||||
| status = status, | |||||
| ) | ) | ||||
| val excelBytes = createStockTakeVarianceExcel( | val excelBytes = createStockTakeVarianceExcel( | ||||
| @@ -422,6 +422,14 @@ open class StockTakeRecordService( | |||||
| .mapNotNull { it.stockTakeSectionDescription } // 先去掉 null | .mapNotNull { it.stockTakeSectionDescription } // 先去掉 null | ||||
| .distinct() // 去重(防止误填多个不同值) | .distinct() // 去重(防止误填多个不同值) | ||||
| .firstOrNull() | .firstOrNull() | ||||
| val warehouseArea = warehouses | |||||
| .mapNotNull { it.area } | |||||
| .distinct() | |||||
| .firstOrNull() | |||||
| val storeId = warehouses | |||||
| .mapNotNull { it.store_id } | |||||
| .distinct() | |||||
| .firstOrNull() | |||||
| val roundIdForLatest = latestStockTake?.let { st -> st.stockTakeRoundId ?: st.id } | val roundIdForLatest = latestStockTake?.let { st -> st.stockTakeRoundId ?: st.id } | ||||
| val roundRecordsForSection = if (latestStockTake != null && roundIdForLatest != null) { | val roundRecordsForSection = if (latestStockTake != null && roundIdForLatest != null) { | ||||
| @@ -483,7 +491,9 @@ open class StockTakeRecordService( | |||||
| endTime = latestStockTake?.actualEnd, | endTime = latestStockTake?.actualEnd, | ||||
| ReStockTakeTrueFalse = reStockTakeTrueFalse, | ReStockTakeTrueFalse = reStockTakeTrueFalse, | ||||
| planStartDate = latestStockTake?.planStart?.toLocalDate(), | planStartDate = latestStockTake?.planStart?.toLocalDate(), | ||||
| stockTakeSectionDescription = sectionDescription | |||||
| stockTakeSectionDescription = sectionDescription, | |||||
| warehouseArea = warehouseArea, | |||||
| storeId = storeId | |||||
| ) | ) | ||||
| ) | ) | ||||
| @@ -804,7 +814,9 @@ open class StockTakeRecordService( | |||||
| endTime = latestBaseStockTake.actualEnd, | endTime = latestBaseStockTake.actualEnd, | ||||
| ReStockTakeTrueFalse = anyNotMatch, | ReStockTakeTrueFalse = anyNotMatch, | ||||
| planStartDate = latestBaseStockTake.planStart?.toLocalDate(), | planStartDate = latestBaseStockTake.planStart?.toLocalDate(), | ||||
| stockTakeSectionDescription = null | |||||
| stockTakeSectionDescription = null, | |||||
| warehouseArea = null, | |||||
| storeId = null | |||||
| ) | ) | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -839,7 +851,9 @@ open class StockTakeRecordService( | |||||
| endTime = latestBaseStockTake.actualEnd, | endTime = latestBaseStockTake.actualEnd, | ||||
| ReStockTakeTrueFalse = false, | ReStockTakeTrueFalse = false, | ||||
| planStartDate = latestBaseStockTake.planStart?.toLocalDate(), | planStartDate = latestBaseStockTake.planStart?.toLocalDate(), | ||||
| stockTakeSectionDescription = null | |||||
| stockTakeSectionDescription = null, | |||||
| warehouseArea = null, | |||||
| storeId = null | |||||
| ) | ) | ||||
| } | } | ||||
| @@ -29,7 +29,10 @@ class StockTakeRecordController( | |||||
| @RequestParam(required = false, defaultValue = "0") pageNum: Int, | @RequestParam(required = false, defaultValue = "0") pageNum: Int, | ||||
| @RequestParam(required = false, defaultValue = "6") pageSize: Int, | @RequestParam(required = false, defaultValue = "6") pageSize: Int, | ||||
| @RequestParam(required = false) sectionDescription: String?, | @RequestParam(required = false) sectionDescription: String?, | ||||
| @RequestParam(required = false) stockTakeSections: String? | |||||
| @RequestParam(required = false) stockTakeSections: String?, | |||||
| @RequestParam(required = false) status: String?, | |||||
| @RequestParam(required = false) area: String?, | |||||
| @RequestParam(required = false) storeId: String? | |||||
| ): RecordsRes<AllPickedStockTakeListReponse> { | ): RecordsRes<AllPickedStockTakeListReponse> { | ||||
| var all = stockOutRecordService.AllPickedStockTakeList() | var all = stockOutRecordService.AllPickedStockTakeList() | ||||
| if (sectionDescription != null && sectionDescription != "All") { | if (sectionDescription != null && sectionDescription != "All") { | ||||
| @@ -46,6 +49,28 @@ class StockTakeRecordController( | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| if (!status.isNullOrBlank() && status != "All") { | |||||
| val normalizedStatus = status.trim().lowercase() | |||||
| val acceptedStatuses = when (normalizedStatus) { | |||||
| "stocktaking" -> setOf("stocktaking", "processing", "in_progress") | |||||
| else -> setOf(normalizedStatus) | |||||
| } | |||||
| all = all.filter { item -> | |||||
| val itemStatus = item.status.trim().lowercase() | |||||
| itemStatus in acceptedStatuses | |||||
| } | |||||
| } | |||||
| if (!area.isNullOrBlank()) { | |||||
| val areaKeyword = area.trim() | |||||
| all = all.filter { it.warehouseArea?.contains(areaKeyword, ignoreCase = true) == true } | |||||
| } | |||||
| if (!storeId.isNullOrBlank() && storeId != "All") { | |||||
| val storeIdKeyword = storeId.trim() | |||||
| all = all.filter { | |||||
| it.storeId?.equals(storeIdKeyword, ignoreCase = true) == true || | |||||
| it.storeId?.contains(storeIdKeyword, ignoreCase = true) == true | |||||
| } | |||||
| } | |||||
| val total = all.size | val total = all.size | ||||
| val fromIndex = pageNum * pageSize | val fromIndex = pageNum * pageSize | ||||
| val toIndex = minOf(fromIndex + pageSize, total) | val toIndex = minOf(fromIndex + pageSize, total) | ||||
| @@ -27,6 +27,8 @@ data class AllPickedStockTakeListReponse( | |||||
| @JsonFormat(pattern = "yyyy-MM-dd") | @JsonFormat(pattern = "yyyy-MM-dd") | ||||
| val planStartDate: LocalDate?, | val planStartDate: LocalDate?, | ||||
| val stockTakeSectionDescription: String?, | val stockTakeSectionDescription: String?, | ||||
| val warehouseArea: String?, | |||||
| val storeId: String?, | |||||
| ) | ) | ||||
| data class InventoryLotDetailResponse( | data class InventoryLotDetailResponse( | ||||
| val id: Long, | val id: Long, | ||||