From 15c961d5432c0e17c0061a388a2ee677228500c3 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sat, 2 May 2026 21:04:30 +0800 Subject: [PATCH] update truck X and singal relesae --- .../service/DoWorkbenchMainService.kt | 33 +++++- .../service/DoWorkbenchReleaseService.kt | 111 ++++++++++++++---- .../web/DoWorkbenchController.kt | 23 +++- 3 files changed, 140 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt index f03bae9..220d6e9 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt @@ -930,12 +930,17 @@ return MessageResponse( ): List = queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = true) + /** + * @param requiredDeliveryDate when null, uses [LocalDate.now] (calendar today). + * When set, filters `dop.requiredDeliveryDate = :targetDate` (workbench date picker / select day). + */ open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( shopName: String?, storeId: String?, truck: String?, + requiredDeliveryDate: LocalDate? = null, ): List = - queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = false) + queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = false, equalsDeliveryDate = requiredDeliveryDate) /** * Workbench completed tickets: query `delivery_order_pick_order` where `ticketStatus = completed`. @@ -1526,11 +1531,18 @@ return MessageResponse( storeId: String?, truck: String?, beforeToday: Boolean, + equalsDeliveryDate: LocalDate? = null, ): List { val today = LocalDate.now() - val dateClause = - if (beforeToday) " dop.requiredDeliveryDate < :today " else " dop.requiredDeliveryDate = :today " - val params = mutableMapOf("today" to today) + val params = mutableMapOf() + val dateClause = if (beforeToday) { + params["today"] = today + " dop.requiredDeliveryDate < :today " + } else { + val target = equalsDeliveryDate ?: today + params["targetDate"] = target + " dop.requiredDeliveryDate = :targetDate " + } val sqlBuilder = StringBuilder( """ SELECT @@ -1582,7 +1594,8 @@ return MessageResponse( private fun mapRowToReleasedDoPickOrderListItem(row: Map): ReleasedDoPickOrderListItem? { val idKey = row.keys.find { it.equals("id", true) } ?: return null - val id = (row[idKey] as? Number)?.toLong() ?: return null + // MySQL BIGINT may come back as BigInteger, which is not a Kotlin Number — avoid dropping rows. + val id = cellToLong(row[idKey]) ?: return null val rdKey = row.keys.find { it.equals("requiredDeliveryDate", true) } val reqDate = when (val v = rdKey?.let { row[it] }) { null -> null @@ -1615,6 +1628,16 @@ return MessageResponse( ) } + private fun cellToLong(v: Any?): Long? { + if (v == null) return null + return when (v) { + is Number -> v.toLong() + is java.lang.Number -> v.longValue() + is String -> v.trim().toLongOrNull() + else -> v.toString().trim().toLongOrNull() + } + } + /** * DO workbench: header [delivery_order_pick_order] + lines from [pick_order.deliveryOrderPickOrderId] (no do_pick_order_line). */ diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt index 59f1bcd..c138a85 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt @@ -25,6 +25,12 @@ import org.springframework.orm.ObjectOptimisticLockingFailureException private const val WORKBENCH_RELEASE_RETRY_MAX = 3 +/** 與 workbench 車線摘要一致:`車線-X`(預設車)不帶樓層 `storeId`。 */ +private const val WORKBENCH_DEFAULT_TRUCK_LANCE_CODE = "車線-X" + +/** 票號 `TI-B-yyyyMMdd-{floor}-nnn` 中段;無樓層時用 `X` 與 2F/4F 區隔。 */ +private const val WORKBENCH_TICKET_FLOOR_SEGMENT_DEFAULT_TRUCK = "X" + private fun isWorkbenchOptimisticLockFailure(t: Throwable?): Boolean { var c: Throwable? = t while (c != null) { @@ -114,21 +120,32 @@ open class DoWorkbenchReleaseService( } open fun startBatchReleaseAsync(ids: List, userId: Long): MessageResponse = - startBatchReleaseAsyncInternal(ids, userId, useV2 = false) + startBatchReleaseAsyncInternal(ids, userId, useV2 = false, dopReleaseType = "batch") /** * V2: deferred suggested pick / stock out / stock out lines until [DoWorkbenchDopoAssignmentService] assigns the ticket. */ open fun startBatchReleaseAsyncV2(ids: List, userId: Long): MessageResponse = - startBatchReleaseAsyncInternal(ids, userId, useV2 = true) + startBatchReleaseAsyncInternal(ids, userId, useV2 = true, dopReleaseType = "batch") - private fun startBatchReleaseAsyncInternal(ids: List, userId: Long, useV2: Boolean): MessageResponse { + /** + * V2 async for one (or few) DOs: [delivery_order_pick_order.releaseType] = `single`, ticket prefix `TI-S-` (aligned with legacy single DO pick tickets). + */ + open fun startBatchReleaseAsyncSingleV2(ids: List, userId: Long): MessageResponse = + startBatchReleaseAsyncInternal(ids, userId, useV2 = true, dopReleaseType = "single") + + private fun startBatchReleaseAsyncInternal( + ids: List, + userId: Long, + useV2: Boolean, + dopReleaseType: String, + ): MessageResponse { if (ids.isEmpty()) { return MessageResponse( id = null, code = "NO_IDS", name = null, - type = if (useV2) "workbench_batch_release_async_v2" else "workbench_batch_release_async", + type = asyncJobType(useV2, dopReleaseType), message = "No delivery order ids provided", errorPosition = null, entity = null @@ -178,7 +195,7 @@ open class DoWorkbenchReleaseService( } try { - createAndLinkDeliveryOrderPickOrders(successResults) + createAndLinkDeliveryOrderPickOrders(successResults, dopReleaseType) } catch (e: Exception) { // header-link failure shouldn't crash job; status.failed already includes per-DO failures println("❌ workbench createAndLinkDeliveryOrderPickOrders failed: ${e.message}") @@ -207,8 +224,8 @@ open class DoWorkbenchReleaseService( id = null, code = "STARTED", name = null, - type = if (useV2) "workbench_batch_release_async_v2" else "workbench_batch_release_async", - message = if (useV2) "Workbench batch release V2 started" else "Workbench batch release started", + type = asyncJobType(useV2, dopReleaseType), + message = asyncJobMessage(useV2, dopReleaseType), errorPosition = null, entity = mapOf("jobId" to jobId, "total" to ids.size) ) @@ -312,7 +329,7 @@ open class DoWorkbenchReleaseService( } } - val createdHeaders = createAndLinkDeliveryOrderPickOrders(successResults) + val createdHeaders = createAndLinkDeliveryOrderPickOrders(successResults, "batch") if (!useV2) { successResults.forEach { result -> try { @@ -342,14 +359,17 @@ open class DoWorkbenchReleaseService( } /** - * Same visual format as batch DO pick tickets (`DoReleaseCoordinatorService`): `TI-B-yyyyMMdd-2F-001`. - * Allocates the next 3-digit suffix by scanning existing `do_pick_order.ticket_no` and - * `delivery_order_pick_order.ticketNo` with the same prefix (avoids `uk_dopo_ticket_no` clashes). + * `TI-B-yyyyMMdd-2F-001` (batch) or `TI-S-yyyyMMdd-2F-001` (single), same suffix rules as [DoReleaseCoordinatorService] / legacy `do_pick_order`. */ - private fun nextDeliveryOrderPickOrderBatchTicketNo(requiredDate: LocalDate, storeDisplay: String): String { + private fun nextDeliveryOrderPickOrderTicketNo( + requiredDate: LocalDate, + storeDisplay: String, + ticketLetter: String, + ): String { + require(ticketLetter == "B" || ticketLetter == "S") { "ticketLetter must be B or S" } val ymd = requiredDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) val floor = storeDisplay.replace("/", "").trim() - val prefix = "TI-B-$ymd-$floor-" + val prefix = "TI-$ticketLetter-$ymd-$floor-" val sql = """ SELECT ticket_no AS t FROM fpsmsdb.do_pick_order WHERE deleted = 0 AND ticket_no LIKE CONCAT(:prefix, '%') UNION ALL @@ -371,6 +391,32 @@ open class DoWorkbenchReleaseService( return "$prefix${next.toString().padStart(3, '0')}" } + private fun nextDeliveryOrderPickOrderBatchTicketNo(requiredDate: LocalDate, storeDisplay: String): String = + nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "B") + + private fun nextDeliveryOrderPickOrderSingleTicketNo(requiredDate: LocalDate, storeDisplay: String): String = + nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "S") + + private fun asyncJobType(useV2: Boolean, dopReleaseType: String): String { + val single = dopReleaseType.equals("single", ignoreCase = true) + return when { + useV2 && single -> "workbench_single_release_async_v2" + useV2 -> "workbench_batch_release_async_v2" + single -> "workbench_single_release_async" + else -> "workbench_batch_release_async" + } + } + + private fun asyncJobMessage(useV2: Boolean, dopReleaseType: String): String { + val single = dopReleaseType.equals("single", ignoreCase = true) + return when { + useV2 && single -> "Workbench single release V2 started" + useV2 -> "Workbench batch release V2 started" + single -> "Workbench single release started" + else -> "Workbench batch release started" + } + } + private fun getOrderedDeliveryOrderIds(ids: List): List { if (ids.isEmpty()) return emptyList() return try { @@ -388,9 +434,17 @@ open class DoWorkbenchReleaseService( } } - private fun createAndLinkDeliveryOrderPickOrders(results: List): Int { + private fun createAndLinkDeliveryOrderPickOrders( + results: List, + dopReleaseType: String = "batch", + ): Int { if (results.isEmpty()) return 0 + val releaseTypeCol = when (dopReleaseType.lowercase()) { + "single" -> "single" + else -> "batch" + } + val grouped = results.groupBy { listOf( it.shopId?.toString() ?: "", @@ -405,13 +459,29 @@ open class DoWorkbenchReleaseService( var createdHeaders = 0 grouped.values.forEach { group -> val first = group.first() - val storeId = when (first.preferredFloor) { - "2F" -> "2/F" - "4F" -> "4/F" - else -> "2/F" + val isDefaultTruckLane = + first.usedDefaultTruck == true || + first.truckLanceCode?.trim() == WORKBENCH_DEFAULT_TRUCK_LANCE_CODE + val storeId: String? = if (isDefaultTruckLane) { + null + } else { + when (first.preferredFloor) { + "2F" -> "2/F" + "4F" -> "4/F" + else -> "2/F" + } + } + val ticketFloorSegment = if (isDefaultTruckLane) { + WORKBENCH_TICKET_FLOOR_SEGMENT_DEFAULT_TRUCK + } else { + (storeId ?: "2/F").replace("/", "").trim() } val requiredDate = first.estimatedArrivalDate ?: LocalDate.now() - val tempTicket = nextDeliveryOrderPickOrderBatchTicketNo(requiredDate, storeId) + val tempTicket = if (releaseTypeCol == "single") { + nextDeliveryOrderPickOrderSingleTicketNo(requiredDate, ticketFloorSegment) + } else { + nextDeliveryOrderPickOrderBatchTicketNo(requiredDate, ticketFloorSegment) + } val now = LocalDateTime.now() // Column names must match Liquibase `01_alter_stock_take.sql` (camelCase), not snake_case. @@ -425,7 +495,7 @@ open class DoWorkbenchReleaseService( ) VALUES ( :truckId, :shopId, :storeId, :requiredDeliveryDate, :truckDepartureTime, :truckLanceCode, :shopCode, :shopName, :loadingSequence, :ticketNo, - NULL, 'pending', 'batch', NULL, NULL, + NULL, 'pending', :releaseType, NULL, NULL, :created, :createdBy, 0, :modified, :modifiedBy, 0 ) """.trimIndent(), @@ -440,6 +510,7 @@ open class DoWorkbenchReleaseService( "shopName" to first.shopName, "loadingSequence" to first.loadingSequence, "ticketNo" to tempTicket, + "releaseType" to releaseTypeCol, "created" to now, "createdBy" to "system", "modified" to now, diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt index 3a2d8a4..ebd6dab 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt @@ -110,9 +110,15 @@ class DoWorkbenchController( fun getWorkbenchReleasedDoPickOrdersToday( @RequestParam(required = false) shopName: String?, @RequestParam(required = false) storeId: String?, - @RequestParam(required = false) truck: String? + @RequestParam(required = false) truck: String?, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?, ): List { - return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday(shopName, storeId, truck) + return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( + shopName, + storeId, + truck, + requiredDeliveryDate = requiredDate, + ) } @PostMapping("/assign-by-delivery-order-pick-order-id") @@ -158,6 +164,19 @@ class DoWorkbenchController( return doWorkbenchReleaseService.startBatchReleaseAsyncV2(ids, userId) } + /** + * One delivery order, same release pipeline as [startWorkbenchBatchReleaseAsyncV2], but + * [delivery_order_pick_order.releaseType] = `single` and ticket prefix `TI-S-` (not batch / `TI-B-`). + * Body: JSON number (mirrors [DoPickOrderController.startBatchReleaseAsyncSingle]). + */ + @PostMapping("/batch-release/async-single-v2") + fun startWorkbenchBatchReleaseAsyncSingleV2( + @RequestBody doId: Long, + @RequestParam(defaultValue = "1") userId: Long + ): MessageResponse { + return doWorkbenchReleaseService.startBatchReleaseAsyncSingleV2(listOf(doId), userId) + } + /** Synchronous batch release V2 (same semantics as async-v2; for tools / tests). */ @PostMapping("/batch-release/sync-v2") fun workbenchBatchReleaseSyncV2(