diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecordRepository.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecordRepository.kt index b019545..1aff62d 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecordRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoPickOrderRecordRepository.kt @@ -28,6 +28,8 @@ interface DoPickOrderRecordRepository : JpaRepository { fun findByStoreIdAndRequiredDeliveryDateAndTicketStatusIn(storeId: String, requiredDeliveryDate: LocalDate, ticketStatus: List): List fun findByHandledByAndTicketStatusAndDeletedFalse(handledBy: Long, ticketStatus: DoPickOrderStatus): List + fun findByTicketStatusAndDeletedFalse(ticketStatus: DoPickOrderStatus): List + @Query("SELECT d FROM DoPickOrderRecord d WHERE d.deleted = false ORDER BY d.ticketReleaseTime DESC NULLS LAST") fun findAllByDeletedFalseOrderByTicketReleaseTimeDesc(): List diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt index 1645a8f..d06a26f 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoPickOrderService.kt @@ -292,6 +292,84 @@ open class DoPickOrderService( return doPickOrderRepository.findByPickOrderId(pickOrderId) } + /** + * 撤銷使用者對 do_pick_order 的領取:清空 handled_by,狀態回到 pending,並解除關聯 pick_order 的 assignTo。 + * 僅適用於尚未完成的單據。 + */ + @Transactional + open fun revertUserAssignment(doPickOrderId: Long): MessageResponse { + val dpo = doPickOrderRepository.findById(doPickOrderId).orElse(null) + ?: return MessageResponse( + id = null, + code = "NOT_FOUND", + name = null, + type = null, + message = "do_pick_order not found", + errorPosition = null, + entity = null, + ) + if (dpo.ticketStatus == DoPickOrderStatus.completed) { + return MessageResponse( + id = dpo.id, + code = "INVALID_STATUS", + name = dpo.ticketNo, + type = null, + message = "Cannot revert a completed ticket", + errorPosition = null, + entity = null, + ) + } + if (dpo.handledBy == null) { + return MessageResponse( + id = dpo.id, + code = "NOT_ASSIGNED", + name = dpo.ticketNo, + type = null, + message = "No user assignment to revert", + errorPosition = null, + entity = null, + ) + } + + dpo.handledBy = null + dpo.handlerName = null + dpo.ticketStatus = DoPickOrderStatus.pending + dpo.ticketReleaseTime = null + doPickOrderRepository.save(dpo) + + val lines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrderId) + lines.forEach { line -> + val pid = line.pickOrderId ?: return@forEach + pickOrderRepository.findById(pid).ifPresent { po -> + po.assignTo = null + po.status = PickOrderStatus.PENDING + pickOrderRepository.save(po) + } + val records = doPickOrderRecordRepository.findByPickOrderId(pid) + records.forEach { r -> + if (r.ticketStatus != DoPickOrderStatus.completed) { + r.handledBy = null + r.handlerName = null + r.ticketStatus = DoPickOrderStatus.pending + r.ticketReleaseTime = null + } + } + if (records.isNotEmpty()) { + doPickOrderRecordRepository.saveAll(records) + } + } + + return MessageResponse( + id = dpo.id, + code = "SUCCESS", + name = dpo.ticketNo, + type = null, + message = "Assignment reverted", + errorPosition = null, + entity = null, + ) + } + open fun updateDoOrderIdForPickOrder(pickOrderId: Long, doOrderId: Long): List { val doPickOrders = doPickOrderRepository.findByPickOrderId(pickOrderId) doPickOrders.forEach { @@ -672,12 +750,13 @@ open class DoPickOrderService( shopName = doPickOrder.shopName, requiredDeliveryDate = doPickOrder.requiredDeliveryDate, handlerName = doPickOrder.handlerName, - numberOfFGItems = countFGItems(doPickOrder) + numberOfFGItems = countFGItems(doPickOrder), + isActiveDoPickOrder = true, ) } val doPickOrderRecordResponses = doPickOrderRecords.map { record -> - TicketReleaseTableResponse( + TicketReleaseTableResponse( id = record.id, storeId = record.storeId, ticketNo = record.ticketNo, @@ -698,7 +777,8 @@ open class DoPickOrderService( shopName = record.shopName, requiredDeliveryDate = record.requiredDeliveryDate, handlerName = record.handlerName, - numberOfFGItems = countFGItemsFromRecord(record) + numberOfFGItems = countFGItemsFromRecord(record), + isActiveDoPickOrder = false, ) } diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt index f74a198..62a4af0 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoPickOrderController.kt @@ -50,7 +50,8 @@ class DoPickOrderController( private val doReleaseCoordinatorService: DoReleaseCoordinatorService, private val doPickOrderLineRepository: DoPickOrderLineRepository, private val doPickOrderQueryService: DoPickOrderQueryService, - private val doPickOrderAssignmentService: DoPickOrderAssignmentService + private val doPickOrderAssignmentService: DoPickOrderAssignmentService, + private val pickOrderService: PickOrderService, ) { @PostMapping("/assign-by-store") fun assignPickOrderByStore(@RequestBody request: AssignByStoreRequest): MessageResponse { @@ -134,4 +135,16 @@ class DoPickOrderController( return doPickOrderService.getTruckScheduleDashboard(date ?: LocalDate.now()) } + /** 強制完成仍處於進行中的 do_pick_order(僅改狀態,不調整已揀數量) */ + @PostMapping("/force-complete/{doPickOrderId}") + fun forceCompleteDoPickOrder(@PathVariable doPickOrderId: Long): MessageResponse { + return pickOrderService.forceCompleteDoPickOrder(doPickOrderId) + } + + /** 撤銷使用者領取,使單據可再次分配 */ + @PostMapping("/revert-assignment/{doPickOrderId}") + fun revertDoPickOrderAssignment(@PathVariable doPickOrderId: Long): MessageResponse { + return doPickOrderService.revertUserAssignment(doPickOrderId) + } + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt index f5d9f86..1eae138 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/TicketReleaseTableResponse.kt @@ -25,7 +25,9 @@ data class TicketReleaseTableResponse( val shopName: String?, val requiredDeliveryDate: LocalDate?, val handlerName: String?, - val numberOfFGItems: Int = 0 + val numberOfFGItems: Int = 0, + /** true:資料來自進行中的 do_pick_order,id 可作 force-complete / revert-assignment;false:來自已歸檔的 record */ + val isActiveDoPickOrder: Boolean = false, ) data class TruckInfoDto( val id: Long?, 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 e65908b..bffac12 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 @@ -1454,6 +1454,157 @@ open class PickOrderService( } } + /** + * 強制完成 do_pick_order:僅將未完成的 pick_order / pick_order_line 標記為完成(不修改 stock_out_line 數量), + * 並執行與正常完成相同的歸檔(do_pick_order → do_pick_order_record)與 delivery_order 狀態更新。 + * 僅適用於仍存在于 do_pick_order 表中的單據。 + */ + @Transactional(rollbackFor = [Exception::class]) + open fun forceCompleteDoPickOrder(doPickOrderId: Long): MessageResponse { + val dpo = doPickOrderRepository.findById(doPickOrderId).orElse(null) + ?: return MessageResponse( + id = null, + name = "Do pick order not found", + code = "NOT_FOUND", + type = "doPickOrder", + message = "do_pick_order not found (may already be archived)", + errorPosition = null, + ) + if (dpo.ticketStatus == DoPickOrderStatus.completed) { + return MessageResponse( + id = dpo.id, + name = dpo.ticketNo, + code = "ALREADY_COMPLETED", + type = "doPickOrder", + message = "Do pick order already completed", + errorPosition = null, + ) + } + if (dpo.ticketStatus !in listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released)) { + return MessageResponse( + id = dpo.id, + name = dpo.ticketNo, + code = "INVALID_STATUS", + type = "doPickOrder", + message = "Invalid ticket status for force complete", + errorPosition = null, + ) + } + + val dpoLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrderId) + val pickOrderIds = dpoLines.mapNotNull { it.pickOrderId }.distinct() + if (pickOrderIds.isEmpty()) { + return MessageResponse( + id = dpo.id, + name = dpo.ticketNo, + code = "NO_PICK_ORDERS", + type = "doPickOrder", + message = "No pick orders linked to this do_pick_order", + errorPosition = null, + ) + } + + for (pickOrderId in pickOrderIds) { + val pols = pickOrderLineRepository.findByPickOrderId(pickOrderId).filter { !it.deleted } + pols.forEach { pol -> + pol.status = PickOrderLineStatus.COMPLETED + } + if (pols.isNotEmpty()) { + pickOrderLineRepository.saveAll(pols) + } + val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) ?: continue + pickOrder.status = PickOrderStatus.COMPLETED + pickOrder.completeDate = LocalDateTime.now() + pickOrderRepository.save(pickOrder) + + if (pickOrder.jobOrder != null) { + val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrderId) + joPickOrders.forEach { it.ticketCompleteTime = LocalDateTime.now() } + if (joPickOrders.isNotEmpty()) { + joPickOrderRepository.saveAll(joPickOrders) + } + val joPickOrderRecords = joPickOrderRecordRepository.findByPickOrderId(pickOrderId) + joPickOrderRecords.forEach { it.ticketCompleteTime = LocalDateTime.now() } + if (joPickOrderRecords.isNotEmpty()) { + joPickOrderRecordRepository.saveAll(joPickOrderRecords) + } + } + } + + moveDoPickOrderToCompletedRecordAfterForce(dpo) + + return MessageResponse( + id = dpo.id, + code = "SUCCESS", + name = dpo.ticketNo, + type = "doPickOrder", + message = "Force completed", + errorPosition = null, + entity = null, + ) + } + + private fun moveDoPickOrderToCompletedRecordAfterForce(dpo: DoPickOrder) { + val doOrderIdForDelivery = dpo.doOrderId + val prefix = "DN" + val midfix = CodeGenerator.DEFAULT_MIDFIX + val latestCode = doPickOrderRecordRepository.findLatestDeliveryNoteCodeByPrefix("$prefix-$midfix") + val deliveryNoteCode = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode) + + val dpoRecord = DoPickOrderRecord( + recordId = dpo.id, + storeId = dpo.storeId ?: "", + ticketNo = dpo.ticketNo ?: "", + ticketStatus = DoPickOrderStatus.completed, + truckId = dpo.truckId, + truckDepartureTime = dpo.truckDepartureTime, + pickOrderId = dpo.pickOrderId, + doOrderId = dpo.doOrderId, + ticketReleaseTime = dpo.ticketReleaseTime, + shopId = dpo.shopId, + handlerName = dpo.handlerName, + handledBy = dpo.handledBy, + ticketCompleteDateTime = LocalDateTime.now(), + truckLanceCode = dpo.truckLanceCode, + shopCode = dpo.shopCode, + shopName = dpo.shopName, + requiredDeliveryDate = dpo.requiredDeliveryDate, + pickOrderCode = dpo.pickOrderCode, + deliveryOrderCode = dpo.deliveryOrderCode, + deliveryNoteCode = deliveryNoteCode, + loadingSequence = dpo.loadingSequence, + ) + val savedHeader = doPickOrderRecordRepository.save(dpoRecord) + + val lines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(dpo.id!!) + val lineRecords = lines.map { l: DoPickOrderLine -> + DoPickOrderLineRecord().apply { + this.recordId = l.id + this.doPickOrderId = savedHeader.recordId + this.pickOrderId = l.pickOrderId + this.doOrderId = l.doOrderId + this.pickOrderCode = l.pickOrderCode + this.deliveryOrderCode = l.deliveryOrderCode + this.status = l.status + } + } + if (lineRecords.isNotEmpty()) { + doPickOrderLineRecordRepository.saveAll(lineRecords) + } + if (lines.isNotEmpty()) { + doPickOrderLineRepository.deleteAll(lines) + } + doPickOrderRepository.delete(dpo) + + doOrderIdForDelivery?.let { doId -> + val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId) + if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) { + deliveryOrder.status = DeliveryOrderStatus.COMPLETED + deliveryOrderRepository.save(deliveryOrder) + } + } + } + @Transactional(rollbackFor = [java.lang.Exception::class]) open fun checkAndCompletePickOrderByConsoCode(consoCode: String): MessageResponse { try { @@ -4100,72 +4251,73 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto return null } - open fun getCompletedDoPickOrders( - userId: Long, + private fun mapCompletedDoPickOrders( + baseRecords: List, request: GetCompletedDoPickOrdersRequest ): List { return try { val normalizedTargetDate = request.targetDate - ?.takeIf { it.isNotBlank() } - ?.replace("-", "") - println("request.targetDate: $request.targetDate") - println("request.shopName: $request.shopName") - println("request.deliveryNoteCode: $request.deliveryNoteCode") - val completedRecords = doPickOrderRecordRepository - .findByHandledByAndTicketStatusAndDeletedFalse(userId, DoPickOrderStatus.completed) - .filter { record -> - val matchTargetDate = normalizedTargetDate.isNullOrBlank() || - record.ticketCompleteDateTime - ?.format(DateTimeFormatter.ofPattern("yyyyMMdd")) - ?.contains(normalizedTargetDate, ignoreCase = true) == true - val matchShop = request.shopName.isNullOrBlank() || - record.shopName?.contains(request.shopName, ignoreCase = true) == true - val matchDeliveryNoteCode = request.deliveryNoteCode.isNullOrBlank() || - record.deliveryNoteCode?.contains(request.deliveryNoteCode, ignoreCase = true) == true - matchTargetDate && matchShop && matchDeliveryNoteCode - } - .filter { (it.recordId ?: 0L) > 0 } - + ?.takeIf { it.isNotBlank() } + ?.replace("-", "") + println("request.targetDate: ${request.targetDate}") + println("request.shopName: ${request.shopName}") + println("request.deliveryNoteCode: ${request.deliveryNoteCode}") + val completedRecords = baseRecords + .filter { record -> + val matchTargetDate = normalizedTargetDate.isNullOrBlank() || + record.ticketCompleteDateTime + ?.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ?.contains(normalizedTargetDate, ignoreCase = true) == true + val matchShop = request.shopName.isNullOrBlank() || + record.shopName?.contains(request.shopName, ignoreCase = true) == true + val matchDeliveryNoteCode = request.deliveryNoteCode.isNullOrBlank() || + record.deliveryNoteCode?.contains(request.deliveryNoteCode, ignoreCase = true) == true + val matchTruck = request.truckLanceCode.isNullOrBlank() || + record.truckLanceCode?.contains(request.truckLanceCode!!, ignoreCase = true) == true + matchTargetDate && matchShop && matchDeliveryNoteCode && matchTruck + } + .filter { (it.recordId ?: 0L) > 0 } + if (completedRecords.isEmpty()) { return emptyList() } - + val recordIds = completedRecords.mapNotNull { it.recordId }.distinct() val lineRecords = doPickOrderLineRecordRepository.findByDoPickOrderIdInAndDeletedFalse(recordIds) val lineRecordsByRecordId = lineRecords.groupBy { it.doPickOrderId } - + val filteredRecords = completedRecords.filter { record -> val lines = lineRecordsByRecordId[record.recordId] ?: emptyList() - + val matchTargetDate = normalizedTargetDate.isNullOrBlank() || record.ticketCompleteDateTime ?.format(DateTimeFormatter.ofPattern("yyyyMMdd")) ?.contains(normalizedTargetDate, ignoreCase = true) == true matchTargetDate }.sortedByDescending { it.ticketCompleteDateTime } - + if (filteredRecords.isEmpty()) { return emptyList() } - + val allPickOrderIds = lineRecords.mapNotNull { it.pickOrderId }.distinct() val pickOrdersById = pickOrderRepository.findAllById(allPickOrderIds).associateBy { it.id!! } - + val allDeliveryOrderIds = lineRecords.mapNotNull { it.doOrderId }.distinct() val deliveryOrdersById = deliveryOrderRepository.findAllById(allDeliveryOrderIds).associateBy { it.id!! } - + filteredRecords.map { record -> val lines = lineRecordsByRecordId[record.recordId] ?: emptyList() val pickOrderIds = lines.mapNotNull { it.pickOrderId }.distinct() val pickOrderCodes = lines.mapNotNull { it.pickOrderCode }.distinct() val deliveryOrderIds = lines.mapNotNull { it.doOrderId }.distinct() val deliveryNos = lines.mapNotNull { it.deliveryOrderCode }.distinct() - + val numberOfCartons = pickOrderIds.sumOf { id -> pickOrdersById[id]?.pickOrderLines?.count { !it.deleted } ?: 0 } val completedDateStr = record.ticketCompleteDateTime - ?.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ?.format(DateTimeFormatter.ofPattern("yyyyMMdd")) val representativePickOrder = pickOrderIds.firstOrNull()?.let { pickOrdersById[it] } val representativeDelivery = deliveryOrderIds.firstOrNull()?.let { deliveryOrdersById[it] } @@ -4183,7 +4335,7 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto shopName = record.shopName, truckLanceCode = record.truckLanceCode, departureTime = record.truckDepartureTime, - deliveryNoteCode=record.deliveryNoteCode, + deliveryNoteCode = record.deliveryNoteCode, pickOrderIds = pickOrderIds, pickOrderCodes = pickOrderCodes, deliveryOrderIds = deliveryOrderIds, @@ -4203,12 +4355,29 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto ) } } catch (e: Exception) { - println("❌ Error in getCompletedDoPickOrders: ${e.message}") + println("❌ Error in mapCompletedDoPickOrders: ${e.message}") e.printStackTrace() emptyList() } } + open fun getCompletedDoPickOrders( + userId: Long, + request: GetCompletedDoPickOrdersRequest + ): List { + val baseRecords = doPickOrderRecordRepository + .findByHandledByAndTicketStatusAndDeletedFalse(userId, DoPickOrderStatus.completed) + return mapCompletedDoPickOrders(baseRecords, request) + } + + open fun getCompletedDoPickOrdersAll( + request: GetCompletedDoPickOrdersRequest + ): List { + val baseRecords = doPickOrderRecordRepository + .findByTicketStatusAndDeletedFalse(DoPickOrderStatus.completed) + return mapCompletedDoPickOrders(baseRecords, request) + } + private fun numToBigDecimal(n: Number?): BigDecimal { return when (n) { diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt index 82a0547..f74c5c8 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt @@ -300,14 +300,32 @@ fun getCompletedDoPickOrders( @RequestParam(required = false) shopName: String?, @RequestParam(required = false) targetDate: String?, @RequestParam(required = false) deliveryNoteCode: String?, + @RequestParam(required = false) truckLanceCode: String?, ): List { val request = GetCompletedDoPickOrdersRequest( targetDate = targetDate, shopName = shopName, - deliveryNoteCode = deliveryNoteCode + deliveryNoteCode = deliveryNoteCode, + truckLanceCode = truckLanceCode, ) return pickOrderService.getCompletedDoPickOrders(userId, request) } +@GetMapping("/completed-do-pick-orders-all") +fun getCompletedDoPickOrdersAll( + @RequestParam(required = false) shopName: String?, + @RequestParam(required = false) targetDate: String?, + @RequestParam(required = false) deliveryNoteCode: String?, + @RequestParam(required = false) truckLanceCode: String?, +): List { + val request = GetCompletedDoPickOrdersRequest( + targetDate = targetDate, + shopName = shopName, + deliveryNoteCode = deliveryNoteCode, + truckLanceCode = truckLanceCode, + ) + return pickOrderService.getCompletedDoPickOrdersAll(request) +} + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SearchPickOrderRequest.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SearchPickOrderRequest.kt index e20f7da..a460ea5 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SearchPickOrderRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SearchPickOrderRequest.kt @@ -19,6 +19,8 @@ data class GetCompletedDoPickOrdersRequest( val targetDate: String? = null, val shopName: String? = null, val deliveryNoteCode: String? = null, + /** 卡車 / 車道代碼(模糊匹配 truck_lance_code) */ + val truckLanceCode: String? = null, ) data class CompletedDoPickOrderResponse( val id: Long,