diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderLineInfo.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderLineInfo.kt index 1fd8a3e1..98145832 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderLineInfo.kt +++ b/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 diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index cee583ca..dd4ccdf1 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/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, pickOrderIdByDoId: Map, pickOrderLines: List, - ): 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 = emptySet(), + ): Map, 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, - pickOrderLines: List, stockOutLinesByPickOrderLineId: Map>, illById: Map, itemsById: Map, @@ -1693,6 +1724,7 @@ open class DeliveryOrderService( headerTicketNo: String? = null, headerIsMerge: Boolean = false, replenishPdfIndex: DoReplenishmentService.ReplenishPdfIndex = DoReplenishmentService.ReplenishPdfIndex.EMPTY, + pickOrderLineIdByDeliveryLine: Map, Long> = emptyMap(), ): MutableMap { val field = mutableMapOf() 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, ): List { @@ -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, + floorSuppliers4F: List, + truckCache: Map, Truck?>?, + ticketLaneByDoId: Map, + ): 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): Map { + val ids = doIds.distinct() + if (ids.isEmpty()) return emptyMap() + val best = HashMap() + 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, + pickOrderIdByDoId: Map, + pickLines: List, + replenishmentPickOrderLineIds: Set = emptySet(), + ): Map, Long> { + val result = mutableMapOf, 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 + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt index 81d0010a..50e8b0e2 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt +++ b/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 = pickOrderLineIds + companion object { val EMPTY = ReplenishPdfIndex(emptySet(), emptySet()) } 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 3b869d9f..686d9643 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 @@ -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 { val deliveryNote = deliveryNoteJasperTemplate val fields = mutableListOf>() @@ -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, ), ) } diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLog.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLog.kt index 40acce88..977bfb0a 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLog.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckLaneRenameLog.kt @@ -29,4 +29,8 @@ open class TruckLaneRenameLog : BaseEntity() { @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 } diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameService.kt index 74e8e800..fa4dc2c6 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckLaneRenameService.kt +++ b/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, time: LocalTime?): Map { - 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 = 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 diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/TruckLaneRenameModels.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/TruckLaneRenameModels.kt index b4e9449d..78b659e4 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/TruckLaneRenameModels.kt +++ b/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, ) diff --git a/src/main/resources/db/changelog/changes/20261028_truck_lane_rename_log_truck_ids/01_add_truck_ids.sql b/src/main/resources/db/changelog/changes/20261028_truck_lane_rename_log_truck_ids/01_add_truck_ids.sql new file mode 100644 index 00000000..f45dba7e --- /dev/null +++ b/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`;