Преглед на файлове

update truck X and singal relesae

production
CANCERYS\kw093 преди 5 дни
родител
ревизия
15c961d543
променени са 3 файла, в които са добавени 140 реда и са изтрити 27 реда
  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 Целия файл

@@ -930,12 +930,17 @@ return MessageResponse(
): List<ReleasedDoPickOrderListItem> =
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<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`.
@@ -1526,11 +1531,18 @@ return MessageResponse(
storeId: String?,
truck: String?,
beforeToday: Boolean,
equalsDeliveryDate: LocalDate? = null,
): List<ReleasedDoPickOrderListItem> {
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(
"""
SELECT
@@ -1582,7 +1594,8 @@ return MessageResponse(

private fun mapRowToReleasedDoPickOrderListItem(row: Map<String, Any?>): 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).
*/


+ 91
- 20
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<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.
*/
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()) {
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<Long>): List<Long> {
if (ids.isEmpty()) return emptyList()
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

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,


+ 21
- 2
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<ReleasedDoPickOrderListItem> {
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(


Зареждане…
Отказ
Запис