Просмотр исходного кода

Merge branch 'production' into stockTakeUpdate

stockTakeUpdate
CANCERYS\kw093 6 дней назад
Родитель
Сommit
47c48b54ae
8 измененных файлов: 321 добавлений и 124 удалений
  1. +1
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderLineInfo.kt
  2. +289
    -79
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  3. +3
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt
  4. +8
    -2
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  5. +4
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLog.kt
  6. +6
    -43
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameService.kt
  7. +1
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/TruckLaneRenameModels.kt
  8. +9
    -0
      src/main/resources/db/changelog/changes/20261028_truck_lane_rename_log_truck_ids/01_add_truck_ids.sql

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderLineInfo.kt Просмотреть файл

@@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Value
import java.math.BigDecimal

interface DeliveryOrderLineInfo {
val id: Long
@get:Value("#{target.item?.name}")
val itemName: String?
val itemNo: String


+ 289
- 79
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Просмотреть файл

@@ -159,6 +159,7 @@ open class DeliveryOrderService(
}
}

/** FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28 */
open fun searchDoLiteByPage(
code: String?,
shopName: String?,
@@ -203,10 +204,14 @@ open class DeliveryOrderService(

val preFilteredContent = allResult.content
val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
val ticketLaneByDoId = loadTicketTruckLanesByDoId(
deliveryOrdersMap.values.filter { isReleasedDeliveryOrder(it) }.mapNotNull { it.id },
)

// ✅ 优化3: 收集所有需要查询的 shopId 和日期组合(只处理预过滤后的记录)
// 未放單才依供應商樓層查 truck;已放單用票據車線
val shopIdAndDatePairs = preFilteredContent.mapNotNull { info ->
val deliveryOrder = deliveryOrdersMap[info.id]
if (isReleasedDeliveryOrder(deliveryOrder)) return@mapNotNull null
val shopId = deliveryOrder?.shop?.id
val estimatedArrivalDate = info.estimatedArrivalDate
if (shopId != null && estimatedArrivalDate != null) {
@@ -234,23 +239,16 @@ open class DeliveryOrderService(
// 处理预过滤后的记录:计算 truckLanceCode 并过滤
val processedRecords = preFilteredContent.map { info ->
val deliveryOrder = deliveryOrdersMap[info.id]
val supplierCode = deliveryOrder?.supplier?.code
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
val shop = deliveryOrder?.shop
val shopId = shop?.id
val estimatedArrivalDate = info.estimatedArrivalDate

val calculatedTruckLanceCode =
if (deliveryOrder != null && shopId != null && estimatedArrivalDate != null) {
val targetDate = estimatedArrivalDate.toLocalDate()
val dayAbbr = getDayOfWeekAbbr(targetDate)

// 从缓存中获取 Truck
val matchedTruck = truckCache[Triple(shopId, preferredFloor, dayAbbr)]
matchedTruck?.truckLanceCode
} else {
null
}
val calculatedTruckLanceCode = displayedTruckLanceCode(
deliveryOrder = deliveryOrder,
estimatedArrivalDate = estimatedArrivalDate,
floorSuppliers2F = floorSuppliers2F,
floorSuppliers4F = floorSuppliers4F,
truckCache = truckCache,
ticketLaneByDoId = ticketLaneByDoId,
)

DeliveryOrderInfoLiteDto(
id = info.id,
@@ -299,22 +297,22 @@ open class DeliveryOrderService(
)

val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
val loadedOrders = result.content.associate { info ->
info.id to deliveryOrderRepository.findByIdAndDeletedIsFalse(info.id)
}
val ticketLaneByDoId = loadTicketTruckLanesByDoId(
loadedOrders.values.filter { isReleasedDeliveryOrder(it) }.mapNotNull { it?.id },
)
val records = result.content.map { info ->
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(info.id)
val supplierCode = deliveryOrder?.supplier?.code
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
val shop = deliveryOrder?.shop
val shopId = shop?.id
val estimatedArrivalDate = info.estimatedArrivalDate

val calculatedTruckLanceCode =
if (deliveryOrder != null && shopId != null && estimatedArrivalDate != null) {
val targetDate = estimatedArrivalDate.toLocalDate()
val dayAbbr = getDayOfWeekAbbr(targetDate)
resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode
} else {
null
}
val deliveryOrder = loadedOrders[info.id]
val calculatedTruckLanceCode = displayedTruckLanceCode(
deliveryOrder = deliveryOrder,
estimatedArrivalDate = info.estimatedArrivalDate,
floorSuppliers2F = floorSuppliers2F,
floorSuppliers4F = floorSuppliers4F,
truckCache = null,
ticketLaneByDoId = ticketLaneByDoId,
)

DeliveryOrderInfoLiteDto(
id = info.id,
@@ -391,8 +389,9 @@ open class DeliveryOrderService(
}

/**
* 僅回傳依店鋪/ETA 從 truck 排程**推算後** `truckLanceCode` 為 null 或空白的送貨單(畫面上對應「車線-X」)。
* 與 [searchDoLiteByPage] 帶一般車線關鍵字分開,避免 `車線-X` 在 truck 表無 shopId 時走舊邏輯漏單。
* FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28
* 僅回傳顯示車線為 null 或空白的送貨單(畫面上對應「車線-X」)。
* 已放單(receiving/completed)用票據車線;未放單才依店鋪/ETA 從 truck 排程推算。
*/
open fun searchDoLiteUnassignedTruckByPage(
code: String?,
@@ -425,23 +424,20 @@ open class DeliveryOrderService(
val deliveryOrderIds = allResult.content.mapNotNull { it.id }
val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds).associateBy { it.id }
val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
val ticketLaneByDoId = loadTicketTruckLanesByDoId(
deliveryOrdersMap.values.filter { isReleasedDeliveryOrder(it) }.mapNotNull { it.id },
)

val processedRecords = allResult.content.map { info ->
val deliveryOrder = deliveryOrdersMap[info.id]
val supplierCode = deliveryOrder?.supplier?.code
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
val shop = deliveryOrder?.shop
val shopId = shop?.id
val infoEta = info.estimatedArrivalDate

val calculatedTruckLanceCode =
if (deliveryOrder != null && shopId != null && infoEta != null) {
val targetDate = infoEta.toLocalDate()
val dayAbbr = getDayOfWeekAbbr(targetDate)
resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode
} else {
null
}
val calculatedTruckLanceCode = displayedTruckLanceCode(
deliveryOrder = deliveryOrder,
estimatedArrivalDate = info.estimatedArrivalDate,
floorSuppliers2F = floorSuppliers2F,
floorSuppliers4F = floorSuppliers4F,
truckCache = null,
ticketLaneByDoId = ticketLaneByDoId,
)

DeliveryOrderInfoLiteDto(
id = info.id,
@@ -1387,6 +1383,7 @@ open class DeliveryOrderService(
)
}

/** FP-MTMS Version Checklist | Functions Ref. No. 89 | v1.0.0 | 2026-10-28 */
private fun exportDeliveryNoteFromRecord(
request: ExportDeliveryNoteRequest,
deliveryNote: net.sf.jasperreports.engine.JasperReport,
@@ -1480,15 +1477,19 @@ open class DeliveryOrderService(
val deliveryOrderCodeById = deliveryOrders.associate { it.id!! to (it.code ?: "") }
val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra }
val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(deliveryOrderIds)
val pickOrderLineIdByDeliveryLine = matchDeliveryNotePickOrderLines(
exportLines = exportLines,
pickOrderIdByDoId = pickOrderIdByDoId,
pickOrderLines = pickOrderLines,
replenishmentPickOrderLineIds = replenishPdfIndex.replenishmentPickOrderLineIds(),
)

sortedLines.forEach { row ->
fields.add(
buildDeliveryNotePdfLineField(
deliveryOrderId = row.deliveryOrderId,
line = row.line,
sequenceNumber = fields.size + 1,
pickOrderIdByDoId = pickOrderIdByDoId,
pickOrderLines = pickOrderLines,
stockOutLinesByPickOrderLineId = stockOutLinesByPickOrderLineId,
illById = illById,
itemsById = itemsById,
@@ -1497,6 +1498,7 @@ open class DeliveryOrderService(
headerTicketNo = null,
headerIsMerge = false,
replenishPdfIndex = replenishPdfIndex,
pickOrderLineIdByDeliveryLine = pickOrderLineIdByDeliveryLine,
),
)
}
@@ -1557,17 +1559,47 @@ open class DeliveryOrderService(
info.deliveryOrderLines.filter { it.deleted != true }.map { line -> DeliveryNoteExportLine(info.id, line) }
}

fun resolvePickOrderLineIdForDeliveryNoteLine(
deliveryOrderId: Long,
itemId: Long?,
/**
* FP-MTMS Version Checklist | Functions Ref. No. 89 | v1.0.0 | 2026-10-28
* Pair each delivery-order line with its own pick line.
* Same item on one pick order is matched by qty first (4↔4, 1↔1).
* Equal qty is paired in id order. Replenishment pick lines are left out;
* those rows still print from do_replenishment.pickOrderLineId.
*/
fun matchDeliveryNotePickOrderLines(
exportLines: List<DeliveryNoteExportLine>,
pickOrderIdByDoId: Map<Long, Long>,
pickOrderLines: List<PickOrderLine>,
): Long? {
if (itemId == null) return null
val pickOrderId = pickOrderIdByDoId[deliveryOrderId] ?: return null
return pickOrderLines
.firstOrNull { pol -> pol.pickOrder?.id == pickOrderId && pol.item?.id == itemId }
?.id
replenishmentPickOrderLineIds: Set<Long> = emptySet(),
): Map<Pair<Long, Long>, Long> {
val keys = exportLines.mapNotNull { row ->
val itemId = row.line.itemId ?: return@mapNotNull null
if (row.line.deleted == true) return@mapNotNull null
DeliveryNoteLineMatchKey(
deliveryOrderId = row.deliveryOrderId,
lineId = row.line.id,
itemId = itemId,
qty = row.line.qty,
)
}
val candidates = pickOrderLines.mapNotNull { pol ->
val polId = pol.id ?: return@mapNotNull null
val pickOrderId = pol.pickOrder?.id ?: return@mapNotNull null
val itemId = pol.item?.id ?: return@mapNotNull null
val qty = pol.qty ?: return@mapNotNull null
DeliveryNotePickLineCandidate(
id = polId,
pickOrderId = pickOrderId,
itemId = itemId,
qty = qty,
)
}
return DeliveryNotePickLineMatcher.match(
lines = keys,
pickOrderIdByDoId = pickOrderIdByDoId,
pickLines = candidates,
replenishmentPickOrderLineIds = replenishmentPickOrderLineIds,
)
}

fun parseTicketTypeLetter(ticketNo: String?): String? {
@@ -1679,12 +1711,11 @@ open class DeliveryOrderService(
return field
}

/** FP-MTMS Version Checklist | Functions Ref. No. 89 | v1.0.0 | 2026-10-28 */
fun buildDeliveryNotePdfLineField(
deliveryOrderId: Long,
line: DeliveryOrderLineInfo,
sequenceNumber: Int,
pickOrderIdByDoId: Map<Long, Long>,
pickOrderLines: List<PickOrderLine>,
stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>>,
illById: Map<Long, InventoryLotLine>,
itemsById: Map<Long, Items>,
@@ -1693,6 +1724,7 @@ open class DeliveryOrderService(
headerTicketNo: String? = null,
headerIsMerge: Boolean = false,
replenishPdfIndex: DoReplenishmentService.ReplenishPdfIndex = DoReplenishmentService.ReplenishPdfIndex.EMPTY,
pickOrderLineIdByDeliveryLine: Map<Pair<Long, Long>, Long> = emptyMap(),
): MutableMap<String, Any> {
val field = mutableMapOf<String, Any>()
val isExtra = isExtraDeliveryTicket(
@@ -1701,12 +1733,7 @@ open class DeliveryOrderService(
headerIsMerge = headerIsMerge,
)

val polId = resolvePickOrderLineIdForDeliveryNoteLine(
deliveryOrderId = deliveryOrderId,
itemId = line.itemId,
pickOrderIdByDoId = pickOrderIdByDoId,
pickOrderLines = pickOrderLines,
)
val polId = pickOrderLineIdByDeliveryLine[deliveryOrderId to line.id]
val isReplenish = replenishPdfIndex.matches(deliveryOrderId, line.itemId, polId)

field["sequenceNumber"] = formatSequenceNumber(sequenceNumber, isExtra, isReplenish)
@@ -2502,6 +2529,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
return out
}

/** FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28 */
private fun buildResolvedTruckDtosForLiteRows(
rows: List<DeliveryOrderInfoLite>,
): List<DeliveryOrderInfoLiteDto> {
@@ -2509,9 +2537,13 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
if (ids.isEmpty()) return emptyList()
val deliveryOrdersMap = deliveryOrderRepository.findAllById(ids).associateBy { it.id }
val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
val ticketLaneByDoId = loadTicketTruckLanesByDoId(
deliveryOrdersMap.values.filter { isReleasedDeliveryOrder(it) }.mapNotNull { it.id },
)

val shopIdAndDatePairs = rows.mapNotNull { info ->
val d = deliveryOrdersMap[info.id]
if (isReleasedDeliveryOrder(d)) return@mapNotNull null
val shopId = d?.shop?.id
val eta = info.estimatedArrivalDate
if (shopId != null && eta != null) {
@@ -2533,18 +2565,14 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }

return rows.map { info ->
val deliveryOrder = deliveryOrdersMap[info.id]
val supplierCode = deliveryOrder?.supplier?.code
val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
val shopId = deliveryOrder?.shop?.id
val infoEta = info.estimatedArrivalDate
val calculatedTruckLanceCode =
if (deliveryOrder != null && shopId != null && infoEta != null) {
val targetDate = infoEta.toLocalDate()
val dayAbbr = getDayOfWeekAbbr(targetDate)
truckCache[Triple(shopId, preferredFloor, dayAbbr)]?.truckLanceCode
} else {
null
}
val calculatedTruckLanceCode = displayedTruckLanceCode(
deliveryOrder = deliveryOrder,
estimatedArrivalDate = info.estimatedArrivalDate,
floorSuppliers2F = floorSuppliers2F,
floorSuppliers4F = floorSuppliers4F,
truckCache = truckCache,
ticketLaneByDoId = ticketLaneByDoId,
)
DeliveryOrderInfoLiteDto(
id = info.id,
code = info.code,
@@ -2560,6 +2588,139 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
}
}

private class TicketLaneHit(
val lane: String?,
val ticketId: Long,
val srcRank: Int,
)

/**
* FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28
* 放單後狀態:receiving 表示已發布,completed 表示已完成。
*/
private fun isReleasedDeliveryOrder(deliveryOrder: DeliveryOrder?): Boolean =
deliveryOrder?.status == DeliveryOrderStatus.RECEIVING ||
deliveryOrder?.status == DeliveryOrderStatus.COMPLETED

/**
* FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28
* 已放單讀票據車線(工作台 `delivery_order_pick_order`,否則 `do_pick_order`/record)。
* 未放單才依供應商樓層 + 店鋪 + 星期從 truck 推算。
* [truckCache] 為 null 時,未放單即時查 truck。
*/
private fun displayedTruckLanceCode(
deliveryOrder: DeliveryOrder?,
estimatedArrivalDate: LocalDateTime?,
floorSuppliers2F: List<String>,
floorSuppliers4F: List<String>,
truckCache: Map<Triple<Long, String, String>, Truck?>?,
ticketLaneByDoId: Map<Long, String?>,
): String? {
if (isReleasedDeliveryOrder(deliveryOrder)) {
return deliveryOrder?.id?.let { ticketLaneByDoId[it] }
}
val shopId = deliveryOrder?.shop?.id ?: return null
val eta = estimatedArrivalDate ?: return null
val preferredFloor = preferredStoreFloorForSupplier(
deliveryOrder.supplier?.code,
floorSuppliers2F,
floorSuppliers4F,
)
val dayAbbr = getDayOfWeekAbbr(eta.toLocalDate())
val key = Triple(shopId, preferredFloor, dayAbbr)
if (truckCache != null) {
return truckCache[key]?.truckLanceCode
}
return resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode
}

/**
* FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28
* 一批送貨單對應的票據車線。同一張單有多張票時,工作台票優先,同來源取 id 最大的一張。
* 票上的車線為空白時仍回傳 null,呼叫端不再改查 truck。
*/
private fun loadTicketTruckLanesByDoId(doIds: Collection<Long>): Map<Long, String?> {
val ids = doIds.distinct()
if (ids.isEmpty()) return emptyMap()
val best = HashMap<Long, TicketLaneHit>()
val sql = """
SELECT do_id, truck_lance_code, ticket_id, src_rank
FROM (
SELECT po.doId AS do_id,
NULLIF(TRIM(dop.truckLanceCode), '') AS truck_lance_code,
dop.id AS ticket_id,
1 AS src_rank
FROM fpsmsdb.delivery_order_pick_order dop
INNER JOIN fpsmsdb.pick_order po
ON po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0
WHERE dop.deleted = 0
AND po.doId IN (:doIds)

UNION ALL

SELECT dpo.do_order_id AS do_id,
NULLIF(TRIM(dpo.TruckLanceCode), '') AS truck_lance_code,
dpo.id AS ticket_id,
2 AS src_rank
FROM fpsmsdb.do_pick_order dpo
WHERE dpo.deleted = 0
AND dpo.do_order_id IN (:doIds)

UNION ALL

SELECT dpol.do_order_id AS do_id,
NULLIF(TRIM(dpo.TruckLanceCode), '') AS truck_lance_code,
dpo.id AS ticket_id,
2 AS src_rank
FROM fpsmsdb.do_pick_order dpo
INNER JOIN fpsmsdb.do_pick_order_line dpol
ON dpol.do_pick_order_id = dpo.id AND dpol.deleted = 0
WHERE dpo.deleted = 0
AND dpol.do_order_id IN (:doIds)

UNION ALL

SELECT rec.do_order_id AS do_id,
NULLIF(TRIM(rec.TruckLanceCode), '') AS truck_lance_code,
rec.id AS ticket_id,
3 AS src_rank
FROM fpsmsdb.do_pick_order_record rec
WHERE rec.deleted = 0
AND rec.do_order_id IN (:doIds)
) lanes
""".trimIndent()

ids.chunked(400).forEach { chunk ->
val rows = jdbcDao.queryForList(sql, mapOf("doIds" to chunk))
for (row in rows) {
val doId = jdbcNumber(row, "do_id")?.toLong() ?: continue
val ticketId = jdbcNumber(row, "ticket_id")?.toLong() ?: 0L
val srcRank = jdbcNumber(row, "src_rank")?.toInt() ?: 9
val lane = jdbcText(row, "truck_lance_code")
val hit = TicketLaneHit(lane, ticketId, srcRank)
val current = best[doId]
if (
current == null ||
hit.srcRank < current.srcRank ||
(hit.srcRank == current.srcRank && hit.ticketId > current.ticketId)
) {
best[doId] = hit
}
}
}
return best.mapValues { it.value.lane }
}

private fun jdbcNumber(row: Map<*, *>, name: String): Number? {
val value = row.entries.firstOrNull { it.key?.toString().equals(name, true) }?.value ?: return null
return value as? Number
}

private fun jdbcText(row: Map<*, *>, name: String): String? {
val value = row.entries.firstOrNull { it.key?.toString().equals(name, true) }?.value ?: return null
return value.toString().trim().takeIf { it.isNotEmpty() }
}

/**
* 依店鋪 + 揀貨樓層解析當日應顯示之車線。
* - **2F**(P07/P06D/P06Y):`TruckLanceCode` 多為 `車線-…` 且不含星期縮寫,不依 `LIKE '%Mon%'` 篩選;取該店該樓層未刪除車輛中出發時間最早者。
@@ -2676,3 +2837,52 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }
}
}
}

internal data class DeliveryNoteLineMatchKey(
val deliveryOrderId: Long,
val lineId: Long,
val itemId: Long,
val qty: BigDecimal,
)

internal data class DeliveryNotePickLineCandidate(
val id: Long,
val pickOrderId: Long,
val itemId: Long,
val qty: BigDecimal,
)

internal object DeliveryNotePickLineMatcher {
/** FP-MTMS Version Checklist | Functions Ref. No. 89 | v1.0.0 | 2026-10-28 */
fun match(
lines: List<DeliveryNoteLineMatchKey>,
pickOrderIdByDoId: Map<Long, Long>,
pickLines: List<DeliveryNotePickLineCandidate>,
replenishmentPickOrderLineIds: Set<Long> = emptySet(),
): Map<Pair<Long, Long>, Long> {
val result = mutableMapOf<Pair<Long, Long>, Long>()
val rowsByDoAndItem = lines.groupBy { it.deliveryOrderId to it.itemId }
for ((key, rows) in rowsByDoAndItem) {
val (deliveryOrderId, itemId) = key
val pickOrderId = pickOrderIdByDoId[deliveryOrderId] ?: continue
val available = pickLines
.filter { pol ->
pol.pickOrderId == pickOrderId &&
pol.itemId == itemId &&
pol.id !in replenishmentPickOrderLineIds
}
.sortedBy { it.id }
.toMutableList()
for (row in rows.sortedBy { it.lineId }) {
val qtyIndex = available.indexOfFirst { it.qty.compareTo(row.qty) == 0 }
val chosen = when {
qtyIndex >= 0 -> available.removeAt(qtyIndex)
available.isNotEmpty() -> available.removeAt(0)
else -> null
} ?: continue
result[row.deliveryOrderId to row.lineId] = chosen.id
}
}
return result
}
}

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt Просмотреть файл

@@ -162,6 +162,9 @@ open class DoReplenishmentService(
return deliveryOrderId to resolvedItemId in targetDoItemKeys
}

/** FP-MTMS Version Checklist | Functions Ref. No. 89 | v1.0.0 | 2026-10-28 */
fun replenishmentPickOrderLineIds(): Set<Long> = pickOrderLineIds

companion object {
val EMPTY = ReplenishPdfIndex(emptySet(), emptySet())
}


+ 8
- 2
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt Просмотреть файл

@@ -3075,6 +3075,7 @@ return MessageResponse(
return deliveryOrderService.formatDoCodesPlain(codes)
}

/** FP-MTMS Version Checklist | Functions Ref. No. 89 | v1.0.0 | 2026-10-28 */
open fun exportDeliveryNoteWorkbench(request: ExportDeliveryNoteRequest): Map<String, Any> {
val deliveryNote = deliveryNoteJasperTemplate
val fields = mutableListOf<MutableMap<String, Any>>()
@@ -3126,6 +3127,12 @@ return MessageResponse(
val headerIsMerge = ctx.header.ticketNo?.startsWith("TI-M-") == true
val headerTicketNo = ctx.header.ticketNo
val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(ctx.deliveryOrderIds)
val pickOrderLineIdByDeliveryLine = deliveryOrderService.matchDeliveryNotePickOrderLines(
exportLines = exportLines,
pickOrderIdByDoId = ctx.pickOrderIdByDoId,
pickOrderLines = pickOrderLines,
replenishmentPickOrderLineIds = replenishPdfIndex.replenishmentPickOrderLineIds(),
)

sortedLines.forEach { row ->
fields.add(
@@ -3133,8 +3140,6 @@ return MessageResponse(
deliveryOrderId = row.deliveryOrderId,
line = row.line,
sequenceNumber = fields.size + 1,
pickOrderIdByDoId = ctx.pickOrderIdByDoId,
pickOrderLines = pickOrderLines,
stockOutLinesByPickOrderLineId = stockOutLinesByPickOrderLineId,
illById = illById,
itemsById = itemsById,
@@ -3143,6 +3148,7 @@ return MessageResponse(
headerTicketNo = headerTicketNo,
headerIsMerge = headerIsMerge,
replenishPdfIndex = replenishPdfIndex,
pickOrderLineIdByDeliveryLine = pickOrderLineIdByDeliveryLine,
),
)
}


+ 4
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLog.kt Просмотреть файл

@@ -29,4 +29,8 @@ open class TruckLaneRenameLog : BaseEntity<Long>() {

@Column(name = "ticketCount", nullable = false)
open var ticketCount: Int = 0

/** `truck.id` of each shop row in the renamed lane, comma-separated, ascending. */
@Column(name = "truckIds", columnDefinition = "TEXT")
open var truckIds: String? = null
}

+ 6
- 43
src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameService.kt Просмотреть файл

@@ -71,20 +71,11 @@ open class TruckLaneRenameService(

val oldTimeLabel = describeTimes(sourceRows.map { it.departureTime })
val casings = sourceRows.map { it.truckLanceCode?.trim().orEmpty() }.filter { it.isNotEmpty() }.distinct()
// Existing delivery tickets (pending / released / assigned / completed) keep the old name.
// Later releases copy the new master name at insert time.
var scheduleCount = 0
var ticketCount = 0
for (casing in casings) {
scheduleCount += applyOpenScheduleRename(casing, newCode, floor, remarkKey)
val params = ticketParams(casing, floor, remarkKey).toMutableMap()
params["newCode"] = newCode
ticketCount += jdbcDao.executeUpdate(
updateTicketSql(DOPO_UPDATE_SQL, req.departureTimeChanged),
withTime(params, newTime),
)
jdbcDao.executeUpdate(
updateTicketSql(PICK_UPDATE_SQL, req.departureTimeChanged),
withTime(params, newTime),
)
}
for (row in sourceRows) {
row.truckLanceCode = newCode
@@ -99,12 +90,13 @@ open class TruckLaneRenameService(
log.remark = remarkKey.ifEmpty { null }
log.oldDepartureTime = oldTimeLabel
log.newDepartureTime = if (req.departureTimeChanged) newTime.toString() else oldTimeLabel
log.ticketCount = ticketCount
log.ticketCount = 0
log.truckIds = sourceRows.mapNotNull { it.id }.distinct().sorted().joinToString(",")
renameLogRepository.save(log)

return RenameTruckLaneResponse(
truckLanceCode = newCode,
ticketCount = ticketCount,
ticketCount = 0,
scheduleLineCount = scheduleCount,
)
}
@@ -139,6 +131,7 @@ open class TruckLaneRenameService(
oldDepartureTime = row.oldDepartureTime,
newDepartureTime = row.newDepartureTime,
ticketCount = row.ticketCount,
truckIds = row.truckIds,
)
}

@@ -196,18 +189,6 @@ open class TruckLaneRenameService(
}
}

private fun withTime(params: MutableMap<String, Any?>, time: LocalTime?): Map<String, Any?> {
if (time != null) params["newTime"] = time.toString()
return params
}

private fun updateTicketSql(base: String, withTime: Boolean): String {
if (!withTime) return base
return base.replace("SET colCode = :newCode", "SET colCode = :newCode, colTime = :newTime")
.replace("SET TruckLanceCode = :newCode", "SET TruckLanceCode = :newCode, truck_departure_time = :newTime")
.replace("SET truckLanceCode = :newCode", "SET truckLanceCode = :newCode, truckDepartureTime = :newTime")
}

private fun ticketParams(code: String, floor: String, remarkKey: String): Map<String, Any?> = mapOf(
"oldCode" to code,
"floor" to floor,
@@ -273,24 +254,6 @@ open class TruckLaneRenameService(
AND ${FLOOR_MATCH.replace("storeCol", "storeId")}
""".trimIndent()

private val DOPO_UPDATE_SQL = """
UPDATE delivery_order_pick_order
SET truckLanceCode = :newCode, version = version + 1
WHERE deleted = 0
AND ticketStatus IN ('pending', 'released')
AND TRIM(truckLanceCode) = :oldCode
AND ${FLOOR_MATCH.replace("storeCol", "storeId")}
""".trimIndent()

private val PICK_UPDATE_SQL = """
UPDATE do_pick_order
SET TruckLanceCode = :newCode, version = version + 1
WHERE deleted = 0
AND ticket_status IN ('pending', 'released')
AND TRIM(TruckLanceCode) = :oldCode
AND ${FLOOR_MATCH.replace("storeCol", "store_id")}
""".trimIndent()

private val OPEN_SCHEDULE_COUNT_SQL = """
SELECT COUNT(*) AS cnt
FROM truck_lane_schedule_line l


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/TruckLaneRenameModels.kt Просмотреть файл

@@ -40,4 +40,5 @@ data class TruckLaneRenameLogResponse(
val oldDepartureTime: String? = null,
val newDepartureTime: String? = null,
val ticketCount: Int = 0,
val truckIds: String? = null,
)

+ 9
- 0
src/main/resources/db/changelog/changes/20261028_truck_lane_rename_log_truck_ids/01_add_truck_ids.sql Просмотреть файл

@@ -0,0 +1,9 @@
-- liquibase formatted sql
-- changeset fpsms:20261028_01_truck_lane_rename_log_truck_ids
-- preconditions onFail:MARK_RAN
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'truck_lane_rename_log' AND column_name = 'truckIds'

-- truck.id values of the shops in the renamed lane, comma-separated, ascending.
-- Nullable so rows written before this column existed stay valid.
ALTER TABLE `truck_lane_rename_log`
ADD COLUMN `truckIds` TEXT NULL DEFAULT NULL AFTER `ticketCount`;

Загрузка…
Отмена
Сохранить