Quellcode durchsuchen

update truck X and singal relesae

production
CANCERYS\kw093 vor 5 Tagen
Ursprung
Commit
15c961d543
3 geänderte Dateien mit 140 neuen und 27 gelöschten Zeilen
  1. +28
    -5
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  2. +91
    -20
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt
  3. +21
    -2
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt

+ 28
- 5
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt Datei anzeigen

@@ -930,12 +930,17 @@ return MessageResponse(
): List<ReleasedDoPickOrderListItem> = ): List<ReleasedDoPickOrderListItem> =
queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = true) 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( open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday(
shopName: String?, shopName: String?,
storeId: String?, storeId: String?,
truck: String?, truck: String?,
requiredDeliveryDate: LocalDate? = null,
): List<ReleasedDoPickOrderListItem> = ): List<ReleasedDoPickOrderListItem> =
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`. * Workbench completed tickets: query `delivery_order_pick_order` where `ticketStatus = completed`.
@@ -1526,11 +1531,18 @@ return MessageResponse(
storeId: String?, storeId: String?,
truck: String?, truck: String?,
beforeToday: Boolean, beforeToday: Boolean,
equalsDeliveryDate: LocalDate? = null,
): List<ReleasedDoPickOrderListItem> { ): List<ReleasedDoPickOrderListItem> {
val today = LocalDate.now() val today = LocalDate.now()
val dateClause =
if (beforeToday) " dop.requiredDeliveryDate < :today " else " dop.requiredDeliveryDate = :today "
val params = mutableMapOf<String, Any>("today" to today)
val params = mutableMapOf<String, Any>()
val dateClause = if (beforeToday) {
params["today"] = today
" dop.requiredDeliveryDate < :today "
} else {
val target = equalsDeliveryDate ?: today
params["targetDate"] = target
" dop.requiredDeliveryDate = :targetDate "
}
val sqlBuilder = StringBuilder( val sqlBuilder = StringBuilder(
""" """
SELECT SELECT
@@ -1582,7 +1594,8 @@ return MessageResponse(


private fun mapRowToReleasedDoPickOrderListItem(row: Map<String, Any?>): ReleasedDoPickOrderListItem? { private fun mapRowToReleasedDoPickOrderListItem(row: Map<String, Any?>): ReleasedDoPickOrderListItem? {
val idKey = row.keys.find { it.equals("id", true) } ?: return null 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 rdKey = row.keys.find { it.equals("requiredDeliveryDate", true) }
val reqDate = when (val v = rdKey?.let { row[it] }) { val reqDate = when (val v = rdKey?.let { row[it] }) {
null -> null 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). * DO workbench: header [delivery_order_pick_order] + lines from [pick_order.deliveryOrderPickOrderId] (no do_pick_order_line).
*/ */


+ 91
- 20
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt Datei anzeigen

@@ -25,6 +25,12 @@ import org.springframework.orm.ObjectOptimisticLockingFailureException


private const val WORKBENCH_RELEASE_RETRY_MAX = 3 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 { private fun isWorkbenchOptimisticLockFailure(t: Throwable?): Boolean {
var c: Throwable? = t var c: Throwable? = t
while (c != null) { while (c != null) {
@@ -114,21 +120,32 @@ open class DoWorkbenchReleaseService(
} }


open fun startBatchReleaseAsync(ids: List<Long>, userId: Long): MessageResponse = open fun startBatchReleaseAsync(ids: List<Long>, 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. * V2: deferred suggested pick / stock out / stock out lines until [DoWorkbenchDopoAssignmentService] assigns the ticket.
*/ */
open fun startBatchReleaseAsyncV2(ids: List<Long>, userId: Long): MessageResponse = open fun startBatchReleaseAsyncV2(ids: List<Long>, userId: Long): MessageResponse =
startBatchReleaseAsyncInternal(ids, userId, useV2 = true)
startBatchReleaseAsyncInternal(ids, userId, useV2 = true, dopReleaseType = "batch")


private fun startBatchReleaseAsyncInternal(ids: List<Long>, 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<Long>, userId: Long): MessageResponse =
startBatchReleaseAsyncInternal(ids, userId, useV2 = true, dopReleaseType = "single")

private fun startBatchReleaseAsyncInternal(
ids: List<Long>,
userId: Long,
useV2: Boolean,
dopReleaseType: String,
): MessageResponse {
if (ids.isEmpty()) { if (ids.isEmpty()) {
return MessageResponse( return MessageResponse(
id = null, id = null,
code = "NO_IDS", code = "NO_IDS",
name = null, 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", message = "No delivery order ids provided",
errorPosition = null, errorPosition = null,
entity = null entity = null
@@ -178,7 +195,7 @@ open class DoWorkbenchReleaseService(
} }


try { try {
createAndLinkDeliveryOrderPickOrders(successResults)
createAndLinkDeliveryOrderPickOrders(successResults, dopReleaseType)
} catch (e: Exception) { } catch (e: Exception) {
// header-link failure shouldn't crash job; status.failed already includes per-DO failures // header-link failure shouldn't crash job; status.failed already includes per-DO failures
println("❌ workbench createAndLinkDeliveryOrderPickOrders failed: ${e.message}") println("❌ workbench createAndLinkDeliveryOrderPickOrders failed: ${e.message}")
@@ -207,8 +224,8 @@ open class DoWorkbenchReleaseService(
id = null, id = null,
code = "STARTED", code = "STARTED",
name = null, 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, errorPosition = null,
entity = mapOf("jobId" to jobId, "total" to ids.size) 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) { if (!useV2) {
successResults.forEach { result -> successResults.forEach { result ->
try { 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 ymd = requiredDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
val floor = storeDisplay.replace("/", "").trim() val floor = storeDisplay.replace("/", "").trim()
val prefix = "TI-B-$ymd-$floor-"
val prefix = "TI-$ticketLetter-$ymd-$floor-"
val sql = """ val sql = """
SELECT ticket_no AS t FROM fpsmsdb.do_pick_order WHERE deleted = 0 AND ticket_no LIKE CONCAT(:prefix, '%') SELECT ticket_no AS t FROM fpsmsdb.do_pick_order WHERE deleted = 0 AND ticket_no LIKE CONCAT(:prefix, '%')
UNION ALL UNION ALL
@@ -371,6 +391,32 @@ open class DoWorkbenchReleaseService(
return "$prefix${next.toString().padStart(3, '0')}" 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<Long>): List<Long> { private fun getOrderedDeliveryOrderIds(ids: List<Long>): List<Long> {
if (ids.isEmpty()) return emptyList() if (ids.isEmpty()) return emptyList()
return try { return try {
@@ -388,9 +434,17 @@ open class DoWorkbenchReleaseService(
} }
} }


private fun createAndLinkDeliveryOrderPickOrders(results: List<ReleaseDoResult>): Int {
private fun createAndLinkDeliveryOrderPickOrders(
results: List<ReleaseDoResult>,
dopReleaseType: String = "batch",
): Int {
if (results.isEmpty()) return 0 if (results.isEmpty()) return 0


val releaseTypeCol = when (dopReleaseType.lowercase()) {
"single" -> "single"
else -> "batch"
}

val grouped = results.groupBy { val grouped = results.groupBy {
listOf( listOf(
it.shopId?.toString() ?: "", it.shopId?.toString() ?: "",
@@ -405,13 +459,29 @@ open class DoWorkbenchReleaseService(
var createdHeaders = 0 var createdHeaders = 0
grouped.values.forEach { group -> grouped.values.forEach { group ->
val first = group.first() 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 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() val now = LocalDateTime.now()


// Column names must match Liquibase `01_alter_stock_take.sql` (camelCase), not snake_case. // Column names must match Liquibase `01_alter_stock_take.sql` (camelCase), not snake_case.
@@ -425,7 +495,7 @@ open class DoWorkbenchReleaseService(
) VALUES ( ) VALUES (
:truckId, :shopId, :storeId, :requiredDeliveryDate, :truckDepartureTime, :truckId, :shopId, :storeId, :requiredDeliveryDate, :truckDepartureTime,
:truckLanceCode, :shopCode, :shopName, :loadingSequence, :ticketNo, :truckLanceCode, :shopCode, :shopName, :loadingSequence, :ticketNo,
NULL, 'pending', 'batch', NULL, NULL,
NULL, 'pending', :releaseType, NULL, NULL,
:created, :createdBy, 0, :modified, :modifiedBy, 0 :created, :createdBy, 0, :modified, :modifiedBy, 0
) )
""".trimIndent(), """.trimIndent(),
@@ -440,6 +510,7 @@ open class DoWorkbenchReleaseService(
"shopName" to first.shopName, "shopName" to first.shopName,
"loadingSequence" to first.loadingSequence, "loadingSequence" to first.loadingSequence,
"ticketNo" to tempTicket, "ticketNo" to tempTicket,
"releaseType" to releaseTypeCol,
"created" to now, "created" to now,
"createdBy" to "system", "createdBy" to "system",
"modified" to now, "modified" to now,


+ 21
- 2
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt Datei anzeigen

@@ -110,9 +110,15 @@ class DoWorkbenchController(
fun getWorkbenchReleasedDoPickOrdersToday( fun getWorkbenchReleasedDoPickOrdersToday(
@RequestParam(required = false) shopName: String?, @RequestParam(required = false) shopName: String?,
@RequestParam(required = false) storeId: 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<ReleasedDoPickOrderListItem> { ): List<ReleasedDoPickOrderListItem> {
return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday(shopName, storeId, truck)
return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday(
shopName,
storeId,
truck,
requiredDeliveryDate = requiredDate,
)
} }


@PostMapping("/assign-by-delivery-order-pick-order-id") @PostMapping("/assign-by-delivery-order-pick-order-id")
@@ -158,6 +164,19 @@ class DoWorkbenchController(
return doWorkbenchReleaseService.startBatchReleaseAsyncV2(ids, userId) 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). */ /** Synchronous batch release V2 (same semantics as async-v2; for tools / tests). */
@PostMapping("/batch-release/sync-v2") @PostMapping("/batch-release/sync-v2")
fun workbenchBatchReleaseSyncV2( fun workbenchBatchReleaseSyncV2(


Laden…
Abbrechen
Speichern