|
|
|
@@ -359,7 +359,7 @@ open class DoWorkbenchReleaseService( |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* `TI-B-yyyyMMdd-2F-001` (batch), `TI-S-yyyyMMdd-2F-001` (single), or `TI-E-yyyyMMdd-2F-001` (Etra), |
|
|
|
* `TI-B-yyyyMMdd-2F-001` (batch / isExtrabatch) or `TI-S-yyyyMMdd-2F-001` (single / isExtrasingle), |
|
|
|
* same suffix rules as [DoReleaseCoordinatorService] / legacy `do_pick_order`. |
|
|
|
*/ |
|
|
|
private fun nextDeliveryOrderPickOrderTicketNo( |
|
|
|
@@ -367,8 +367,8 @@ open class DoWorkbenchReleaseService( |
|
|
|
storeDisplay: String, |
|
|
|
ticketLetter: String, |
|
|
|
): String { |
|
|
|
require(ticketLetter == "B" || ticketLetter == "S" || ticketLetter == "E") { |
|
|
|
"ticketLetter must be B, S or E" |
|
|
|
require(ticketLetter == "B" || ticketLetter == "S") { |
|
|
|
"ticketLetter must be B or S" |
|
|
|
} |
|
|
|
val ymd = requiredDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) |
|
|
|
val floor = storeDisplay.replace("/", "").trim() |
|
|
|
@@ -400,9 +400,6 @@ open class DoWorkbenchReleaseService( |
|
|
|
private fun nextDeliveryOrderPickOrderSingleTicketNo(requiredDate: LocalDate, storeDisplay: String): String = |
|
|
|
nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "S") |
|
|
|
|
|
|
|
private fun nextDeliveryOrderPickOrderEtraTicketNo(requiredDate: LocalDate, storeDisplay: String): String = |
|
|
|
nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "E") |
|
|
|
|
|
|
|
private fun asyncJobType(useV2: Boolean, dopReleaseType: String): String { |
|
|
|
val single = dopReleaseType.equals("single", ignoreCase = true) |
|
|
|
return when { |
|
|
|
@@ -440,120 +437,267 @@ open class DoWorkbenchReleaseService( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun laneGroupKey(result: ReleaseDoResult): String = |
|
|
|
listOf( |
|
|
|
result.shopId?.toString() ?: "", |
|
|
|
result.estimatedArrivalDate?.toString() ?: "", |
|
|
|
result.preferredFloor, |
|
|
|
result.truckId?.toString() ?: "", |
|
|
|
result.truckDepartureTime?.toString() ?: "", |
|
|
|
result.truckLanceCode ?: "", |
|
|
|
result.loadingSequence?.toString() ?: "", |
|
|
|
).joinToString("|") |
|
|
|
|
|
|
|
private fun resolveStoreId(first: ReleaseDoResult): String? { |
|
|
|
val isDefaultTruckLane = |
|
|
|
first.usedDefaultTruck == true || |
|
|
|
first.truckLanceCode?.trim() == WORKBENCH_DEFAULT_TRUCK_LANCE_CODE |
|
|
|
if (isDefaultTruckLane) return null |
|
|
|
return when (first.preferredFloor) { |
|
|
|
"2F" -> "2/F" |
|
|
|
"4F" -> "4/F" |
|
|
|
else -> "2/F" |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun resolveTicketFloorSegment(storeId: String?, isDefaultTruckLane: Boolean): String = |
|
|
|
if (isDefaultTruckLane) { |
|
|
|
WORKBENCH_TICKET_FLOOR_SEGMENT_DEFAULT_TRUCK |
|
|
|
} else { |
|
|
|
(storeId ?: "2/F").replace("/", "").trim() |
|
|
|
} |
|
|
|
|
|
|
|
private data class MergeableWorkbenchTicket(val id: Long, val releaseType: String?) |
|
|
|
|
|
|
|
/** |
|
|
|
* Active batch/single-family ticket for the same lane. Excludes completed tickets and legacy `isExtra`. |
|
|
|
*/ |
|
|
|
private fun findMergeableWorkbenchTicket( |
|
|
|
first: ReleaseDoResult, |
|
|
|
storeId: String?, |
|
|
|
isSingleRelease: Boolean, |
|
|
|
): MergeableWorkbenchTicket? { |
|
|
|
if (first.shopId == null || first.estimatedArrivalDate == null) return null |
|
|
|
|
|
|
|
val familyTypes = if (isSingleRelease) { |
|
|
|
WorkbenchReleaseTypeSupport.singleFamilyTypes() |
|
|
|
} else { |
|
|
|
WorkbenchReleaseTypeSupport.batchFamilyTypes() |
|
|
|
} |
|
|
|
val typePlaceholders = familyTypes.joinToString(",") { "'${it.lowercase()}'" } |
|
|
|
|
|
|
|
val sql = StringBuilder( |
|
|
|
""" |
|
|
|
SELECT id, releaseType |
|
|
|
FROM fpsmsdb.delivery_order_pick_order |
|
|
|
WHERE deleted = 0 |
|
|
|
AND shopId = :shopId |
|
|
|
AND requiredDeliveryDate = :requiredDate |
|
|
|
AND ticketStatus IN ('pending', 'released') |
|
|
|
AND LOWER(COALESCE(releaseType, '')) IN ($typePlaceholders) |
|
|
|
""".trimIndent() |
|
|
|
) |
|
|
|
val params = mutableMapOf<String, Any>( |
|
|
|
"shopId" to first.shopId!!, |
|
|
|
"requiredDate" to first.estimatedArrivalDate!!, |
|
|
|
) |
|
|
|
if (storeId.isNullOrBlank()) { |
|
|
|
sql.append(" AND (storeId IS NULL OR TRIM(COALESCE(storeId, '')) = '') ") |
|
|
|
} else { |
|
|
|
sql.append(" AND storeId = :storeId ") |
|
|
|
params["storeId"] = storeId |
|
|
|
} |
|
|
|
if (first.truckId != null) { |
|
|
|
sql.append(" AND truckId = :truckId ") |
|
|
|
params["truckId"] = first.truckId!! |
|
|
|
} else { |
|
|
|
sql.append(" AND truckId IS NULL ") |
|
|
|
} |
|
|
|
if (first.truckDepartureTime != null) { |
|
|
|
sql.append(" AND truckDepartureTime = :truckDepartureTime ") |
|
|
|
params["truckDepartureTime"] = first.truckDepartureTime!! |
|
|
|
} else { |
|
|
|
sql.append(" AND truckDepartureTime IS NULL ") |
|
|
|
} |
|
|
|
if (!first.truckLanceCode.isNullOrBlank()) { |
|
|
|
sql.append(" AND truckLanceCode = :truckLanceCode ") |
|
|
|
params["truckLanceCode"] = first.truckLanceCode!!.trim() |
|
|
|
} else { |
|
|
|
sql.append(" AND (truckLanceCode IS NULL OR TRIM(truckLanceCode) = '') ") |
|
|
|
} |
|
|
|
if (first.loadingSequence != null) { |
|
|
|
sql.append(" AND loadingSequence = :loadingSequence ") |
|
|
|
params["loadingSequence"] = first.loadingSequence!! |
|
|
|
} |
|
|
|
sql.append(" ORDER BY id ASC ") |
|
|
|
|
|
|
|
val rows = try { |
|
|
|
jdbcDao.queryForList(sql.toString(), params) |
|
|
|
} catch (_: Exception) { |
|
|
|
emptyList() |
|
|
|
} |
|
|
|
if (rows.isEmpty()) return null |
|
|
|
|
|
|
|
fun rowId(row: Map<String, Any?>): Long? { |
|
|
|
val key = row.keys.find { it.equals("id", true) } ?: return null |
|
|
|
return (row[key] as? Number)?.toLong() |
|
|
|
} |
|
|
|
fun rowReleaseType(row: Map<String, Any?>): String? { |
|
|
|
val key = row.keys.find { it.equals("releaseType", true) } ?: return null |
|
|
|
return row[key]?.toString() |
|
|
|
} |
|
|
|
|
|
|
|
val matched = rows.first() |
|
|
|
val id = rowId(matched) ?: return null |
|
|
|
return MergeableWorkbenchTicket(id = id, releaseType = rowReleaseType(matched)) |
|
|
|
} |
|
|
|
|
|
|
|
private fun linkPickOrdersToHeader(headerId: Long, group: List<ReleaseDoResult>) { |
|
|
|
group.forEach { r -> |
|
|
|
jdbcDao.executeUpdate( |
|
|
|
""" |
|
|
|
UPDATE fpsmsdb.pick_order |
|
|
|
SET deliveryOrderPickOrderId = :headerId |
|
|
|
WHERE id = :pickOrderId |
|
|
|
""".trimIndent(), |
|
|
|
mapOf( |
|
|
|
"headerId" to headerId, |
|
|
|
"pickOrderId" to r.pickOrderId |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun maybeUpgradeReleaseType( |
|
|
|
headerId: Long, |
|
|
|
currentReleaseType: String?, |
|
|
|
isExtraRelease: Boolean, |
|
|
|
isSingleRelease: Boolean, |
|
|
|
) { |
|
|
|
val upgraded = WorkbenchReleaseTypeSupport.upgradedReleaseTypeIfNeeded( |
|
|
|
currentReleaseType, |
|
|
|
isExtraRelease, |
|
|
|
isSingleRelease, |
|
|
|
) ?: return |
|
|
|
jdbcDao.executeUpdate( |
|
|
|
""" |
|
|
|
UPDATE fpsmsdb.delivery_order_pick_order |
|
|
|
SET releaseType = :releaseType, |
|
|
|
modified = :modified, |
|
|
|
modifiedBy = :modifiedBy |
|
|
|
WHERE id = :id AND deleted = 0 |
|
|
|
""".trimIndent(), |
|
|
|
mapOf( |
|
|
|
"releaseType" to upgraded, |
|
|
|
"modified" to LocalDateTime.now(), |
|
|
|
"modifiedBy" to "system", |
|
|
|
"id" to headerId, |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
private fun insertNewDeliveryOrderPickOrderHeader( |
|
|
|
first: ReleaseDoResult, |
|
|
|
storeId: String?, |
|
|
|
releaseTypeCol: String, |
|
|
|
ticketNo: String, |
|
|
|
): Long? { |
|
|
|
val now = LocalDateTime.now() |
|
|
|
jdbcDao.executeUpdate( |
|
|
|
""" |
|
|
|
INSERT INTO fpsmsdb.delivery_order_pick_order ( |
|
|
|
truckId, shopId, storeId, requiredDeliveryDate, truckDepartureTime, |
|
|
|
truckLanceCode, shopCode, shopName, loadingSequence, ticketNo, |
|
|
|
ticketReleaseTime, ticketStatus, releaseType, handledBy, handlerName, |
|
|
|
created, createdBy, version, modified, modifiedBy, deleted |
|
|
|
) VALUES ( |
|
|
|
:truckId, :shopId, :storeId, :requiredDeliveryDate, :truckDepartureTime, |
|
|
|
:truckLanceCode, :shopCode, :shopName, :loadingSequence, :ticketNo, |
|
|
|
NULL, 'pending', :releaseType, NULL, NULL, |
|
|
|
:created, :createdBy, 0, :modified, :modifiedBy, 0 |
|
|
|
) |
|
|
|
""".trimIndent(), |
|
|
|
mapOf( |
|
|
|
"truckId" to first.truckId, |
|
|
|
"shopId" to first.shopId, |
|
|
|
"storeId" to storeId, |
|
|
|
"requiredDeliveryDate" to (first.estimatedArrivalDate ?: LocalDate.now()), |
|
|
|
"truckDepartureTime" to first.truckDepartureTime, |
|
|
|
"truckLanceCode" to first.truckLanceCode, |
|
|
|
"shopCode" to first.shopCode, |
|
|
|
"shopName" to first.shopName, |
|
|
|
"loadingSequence" to first.loadingSequence, |
|
|
|
"ticketNo" to ticketNo, |
|
|
|
"releaseType" to releaseTypeCol, |
|
|
|
"created" to now, |
|
|
|
"createdBy" to "system", |
|
|
|
"modified" to now, |
|
|
|
"modifiedBy" to "system", |
|
|
|
) |
|
|
|
) |
|
|
|
return jdbcDao.queryForList( |
|
|
|
""" |
|
|
|
SELECT id |
|
|
|
FROM fpsmsdb.delivery_order_pick_order |
|
|
|
WHERE ticketNo = :ticketNo |
|
|
|
ORDER BY id DESC |
|
|
|
LIMIT 1 |
|
|
|
""".trimIndent(), |
|
|
|
mapOf("ticketNo" to ticketNo) |
|
|
|
).firstOrNull()?.get("id")?.let { (it as Number).toLong() } |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Link released pick orders to [delivery_order_pick_order] headers. |
|
|
|
* Normal DOs merge into batch/single-family tickets; isExtra DOs upgrade `batch`→`isExtrabatch` or `single`→`isExtrasingle`. |
|
|
|
* Legacy `isExtra` tickets are never merge targets. Completed tickets are skipped (new header created instead). |
|
|
|
*/ |
|
|
|
private fun createAndLinkDeliveryOrderPickOrders( |
|
|
|
results: List<ReleaseDoResult>, |
|
|
|
dopReleaseType: String = "batch", |
|
|
|
): Int { |
|
|
|
if (results.isEmpty()) return 0 |
|
|
|
|
|
|
|
val grouped = results.groupBy { |
|
|
|
listOf( |
|
|
|
it.shopId?.toString() ?: "", |
|
|
|
it.estimatedArrivalDate?.toString() ?: "", |
|
|
|
it.preferredFloor, |
|
|
|
it.truckId?.toString() ?: "", |
|
|
|
it.truckDepartureTime?.toString() ?: "", |
|
|
|
it.truckLanceCode ?: "", |
|
|
|
it.isExtra.toString(), |
|
|
|
).joinToString("|") |
|
|
|
} |
|
|
|
val isSingleRelease = dopReleaseType.equals("single", ignoreCase = true) |
|
|
|
val normalGroups = results.filter { !it.isExtra }.groupBy { laneGroupKey(it) } |
|
|
|
val extraGroups = results.filter { it.isExtra }.groupBy { laneGroupKey(it) } |
|
|
|
|
|
|
|
var createdHeaders = 0 |
|
|
|
grouped.values.forEach { group -> |
|
|
|
|
|
|
|
fun processGroup(group: List<ReleaseDoResult>, isExtraRelease: Boolean): Int { |
|
|
|
if (group.isEmpty()) return 0 |
|
|
|
val first = group.first() |
|
|
|
val storeId = resolveStoreId(first) |
|
|
|
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 ticketFloorSegment = resolveTicketFloorSegment(storeId, isDefaultTruckLane) |
|
|
|
val requiredDate = first.estimatedArrivalDate ?: LocalDate.now() |
|
|
|
val releaseTypeCol = if (first.isExtra) { |
|
|
|
"isExtra" |
|
|
|
} else if (dopReleaseType.equals("single", ignoreCase = true)) { |
|
|
|
"single" |
|
|
|
} else { |
|
|
|
"batch" |
|
|
|
|
|
|
|
val existing = findMergeableWorkbenchTicket(first, storeId, isSingleRelease) |
|
|
|
if (existing != null) { |
|
|
|
linkPickOrdersToHeader(existing.id, group) |
|
|
|
maybeUpgradeReleaseType(existing.id, existing.releaseType, isExtraRelease, isSingleRelease) |
|
|
|
return 0 |
|
|
|
} |
|
|
|
val tempTicket = if (first.isExtra) { |
|
|
|
nextDeliveryOrderPickOrderEtraTicketNo(requiredDate, ticketFloorSegment) |
|
|
|
} else if (releaseTypeCol == "single") { |
|
|
|
|
|
|
|
val releaseTypeCol = WorkbenchReleaseTypeSupport.newHeaderReleaseType(isExtraRelease, isSingleRelease) |
|
|
|
val ticketNo = if (isSingleRelease) { |
|
|
|
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. |
|
|
|
jdbcDao.executeUpdate( |
|
|
|
""" |
|
|
|
INSERT INTO fpsmsdb.delivery_order_pick_order ( |
|
|
|
truckId, shopId, storeId, requiredDeliveryDate, truckDepartureTime, |
|
|
|
truckLanceCode, shopCode, shopName, loadingSequence, ticketNo, |
|
|
|
ticketReleaseTime, ticketStatus, releaseType, handledBy, handlerName, |
|
|
|
created, createdBy, version, modified, modifiedBy, deleted |
|
|
|
) VALUES ( |
|
|
|
:truckId, :shopId, :storeId, :requiredDeliveryDate, :truckDepartureTime, |
|
|
|
:truckLanceCode, :shopCode, :shopName, :loadingSequence, :ticketNo, |
|
|
|
NULL, 'pending', :releaseType, NULL, NULL, |
|
|
|
:created, :createdBy, 0, :modified, :modifiedBy, 0 |
|
|
|
) |
|
|
|
""".trimIndent(), |
|
|
|
mapOf( |
|
|
|
"truckId" to first.truckId, |
|
|
|
"shopId" to first.shopId, |
|
|
|
"storeId" to storeId, |
|
|
|
"requiredDeliveryDate" to (first.estimatedArrivalDate ?: LocalDate.now()), |
|
|
|
"truckDepartureTime" to first.truckDepartureTime, |
|
|
|
"truckLanceCode" to first.truckLanceCode, |
|
|
|
"shopCode" to first.shopCode, |
|
|
|
"shopName" to first.shopName, |
|
|
|
"loadingSequence" to first.loadingSequence, |
|
|
|
"ticketNo" to tempTicket, |
|
|
|
"releaseType" to releaseTypeCol, |
|
|
|
"created" to now, |
|
|
|
"createdBy" to "system", |
|
|
|
"modified" to now, |
|
|
|
"modifiedBy" to "system", |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
val headerId = jdbcDao.queryForList( |
|
|
|
""" |
|
|
|
SELECT id |
|
|
|
FROM fpsmsdb.delivery_order_pick_order |
|
|
|
WHERE ticketNo = :ticketNo |
|
|
|
ORDER BY id DESC |
|
|
|
LIMIT 1 |
|
|
|
""".trimIndent(), |
|
|
|
mapOf("ticketNo" to tempTicket) |
|
|
|
).firstOrNull()?.get("id")?.let { (it as Number).toLong() } ?: return@forEach |
|
|
|
val headerId = insertNewDeliveryOrderPickOrderHeader(first, storeId, releaseTypeCol, ticketNo) |
|
|
|
?: return 0 |
|
|
|
linkPickOrdersToHeader(headerId, group) |
|
|
|
return 1 |
|
|
|
} |
|
|
|
|
|
|
|
group.forEach { r -> |
|
|
|
jdbcDao.executeUpdate( |
|
|
|
""" |
|
|
|
UPDATE fpsmsdb.pick_order |
|
|
|
SET deliveryOrderPickOrderId = :headerId |
|
|
|
WHERE id = :pickOrderId |
|
|
|
""".trimIndent(), |
|
|
|
mapOf( |
|
|
|
"headerId" to headerId, |
|
|
|
"pickOrderId" to r.pickOrderId |
|
|
|
) |
|
|
|
) |
|
|
|
} |
|
|
|
createdHeaders++ |
|
|
|
normalGroups.values.forEach { group -> |
|
|
|
createdHeaders += processGroup(group, isExtraRelease = false) |
|
|
|
} |
|
|
|
extraGroups.values.forEach { group -> |
|
|
|
createdHeaders += processGroup(group, isExtraRelease = true) |
|
|
|
} |
|
|
|
|
|
|
|
return createdHeaders |
|
|
|
|