diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt index 7b3ca0a..e2ab2f4 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt @@ -706,8 +706,11 @@ class DoReleaseCoordinatorService( val existing = candidates.firstOrNull { c -> c.truckId == first.truckId && c.truckDepartureTime == first.truckDepartureTime && - c.truckLanceCode == first.truckLanceCode - } ?: candidates.minByOrNull { it.id ?: Long.MAX_VALUE } + c.truckLanceCode == first.truckLanceCode && + (storeId != null || doPickOrderMatchesTruckXPreferredFloor(c, first.preferredFloor)) + } ?: candidates + .filter { storeId != null || doPickOrderMatchesTruckXPreferredFloor(it, first.preferredFloor) } + .minByOrNull { it.id ?: Long.MAX_VALUE } if (existing != null) { println( " DEBUG: Merging batch into existing DoPickOrder id=${existing.id}, ticket=${existing.ticketNo} " + @@ -754,6 +757,33 @@ class DoReleaseCoordinatorService( println(" DEBUG: Created ${results.size} DoPickOrderLine records") } + + /** + * Truck X (`storeId` null): only merge when every linked DO supplier preferred floor + * matches [preferredFloor] (4F list wins; else 2F). Empty tickets are allowed. + */ + private fun doPickOrderMatchesTruckXPreferredFloor( + dpo: DoPickOrder, + preferredFloor: String?, + ): Boolean { + val target = when (preferredFloor?.trim()?.uppercase(java.util.Locale.ROOT)?.replace("/", "")) { + "4F" -> "4F" + else -> "2F" + } + val id = dpo.id ?: return true + val lines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(id) + if (lines.isEmpty()) return true + val (_, s4f) = doFloorSupplierSettingsService.loadDoFloorSupplierLists() + fun floorOf(supplierCode: String?): String = + if (!supplierCode.isNullOrBlank() && s4f.contains(supplierCode.trim())) "4F" else "2F" + for (line in lines) { + val doId = line.doOrderId ?: continue + val dorder = deliveryOrderRepository.findById(doId).orElse(null) ?: continue + if (floorOf(dorder.supplier?.code) != target) return false + } + return true + } + private fun checkPickOrderHasStockIssues(doPickOrderId: Long): Boolean { try { // 1. 获取 do_pick_order 的所有 pick orders 数量 @@ -858,7 +888,8 @@ class DoReleaseCoordinatorService( .firstOrNull { it.requiredDeliveryDate == result.estimatedArrivalDate && it.truckDepartureTime == result.truckDepartureTime && - it.truckLanceCode == result.truckLanceCode + it.truckLanceCode == result.truckLanceCode && + (storeId != null || doPickOrderMatchesTruckXPreferredFloor(it, result.preferredFloor)) } if (existingDoPickOrder != null) { 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 492d7f1..9377054 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 @@ -134,6 +134,7 @@ open class DoWorkbenchMainService( private val itemsRepository: ItemsRepository, private val transactionManager: PlatformTransactionManager, private val doReplenishmentService: DoReplenishmentService, + private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, ) { @PersistenceContext private lateinit var entityManager: EntityManager @@ -850,6 +851,9 @@ return MessageResponse( ) } + // Truck X keeps storeId null; split count by supplier preferred floor (same as DO Search tabs). + val truckXFloorKey = actualStoreId.replace("/", "").trim().uppercase(java.util.Locale.ROOT) + val truckXFloorClause = truckXPreferredFloorExistsSql(truckXFloorKey) val defaultCountSql = """ SELECT COUNT(DISTINCT dop.id) AS c FROM fpsmsdb.delivery_order_pick_order dop @@ -862,6 +866,7 @@ return MessageResponse( SELECT 1 FROM fpsmsdb.pick_order po WHERE po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0 ) + $truckXFloorClause $releaseFilterClause """.trimIndent() @@ -1033,13 +1038,22 @@ return MessageResponse( storeId: String?, truck: String?, releaseTypeFilter: String? = null, + floor: String? = null, ): List = - queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = true, releaseTypeFilter = releaseTypeFilter) + queryWorkbenchReleasedDopoList( + shopName, + storeId, + truck, + beforeToday = true, + releaseTypeFilter = releaseTypeFilter, + floor = floor, + ) /** * @param requiredDeliveryDate when null, uses [LocalDate.now] (calendar today). * When set, filters `dop.requiredDeliveryDate = :targetDate` (workbench date picker / select day). * @param releaseTypeFilter when `isExtra` (case-insensitive), only `delivery_order_pick_order.releaseType = isExtra` rows. + * @param floor optional `2F`/`4F`: for Truck X (null storeId) filter by DO supplier preferred floor. */ open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( shopName: String?, @@ -1047,6 +1061,7 @@ return MessageResponse( truck: String?, requiredDeliveryDate: LocalDate? = null, releaseTypeFilter: String? = null, + floor: String? = null, ): List = queryWorkbenchReleasedDopoList( shopName, @@ -1055,6 +1070,7 @@ return MessageResponse( beforeToday = false, equalsDeliveryDate = requiredDeliveryDate, releaseTypeFilter = releaseTypeFilter, + floor = floor, ) /** @@ -1648,6 +1664,39 @@ return MessageResponse( ) } + /** + * EXISTS clause: Truck X ticket has at least one DO whose supplier preferred floor matches [floorKey] + * (`2F` / `4F`), and **no** linked DO on the other floor. + * Aligns with [DoFloorSupplierSettingsService.preferredStoreFloorForSupplier] + * (4F list wins; otherwise 2F). Empty string = no extra filter. + */ + private fun truckXPreferredFloorExistsSql(floorKey: String?): String { + val key = floorKey?.trim()?.uppercase(java.util.Locale.ROOT)?.replace("/", "").orEmpty() + if (key != "2F" && key != "4F") return "" + val (_, s4f) = doFloorSupplierSettingsService.loadDoFloorSupplierLists() + val in4 = s4f.joinToString(", ") { "'" + it.replace("'", "''") + "'" } + .ifEmpty { "'__NO_4F_SUPPLIER__'" } + val floorExpr = "CASE WHEN s.code IN ($in4) THEN '4F' ELSE '2F' END" + return """ + AND EXISTS ( + SELECT 1 + FROM fpsmsdb.pick_order po + INNER JOIN fpsmsdb.delivery_order d ON d.id = po.doId AND d.deleted = 0 + LEFT JOIN fpsmsdb.shop s ON s.id = d.supplierId AND s.deleted = 0 + WHERE po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0 + AND ($floorExpr) = '$key' + ) + AND NOT EXISTS ( + SELECT 1 + FROM fpsmsdb.pick_order po2 + INNER JOIN fpsmsdb.delivery_order d2 ON d2.id = po2.doId AND d2.deleted = 0 + LEFT JOIN fpsmsdb.shop s2 ON s2.id = d2.supplierId AND s2.deleted = 0 + WHERE po2.deliveryOrderPickOrderId = dop.id AND po2.deleted = 0 + AND (CASE WHEN s2.code IN ($in4) THEN '4F' ELSE '2F' END) <> '$key' + ) + """.trimIndent() + } + private fun queryWorkbenchReleasedDopoList( shopName: String?, storeId: String?, @@ -1655,6 +1704,7 @@ return MessageResponse( beforeToday: Boolean, equalsDeliveryDate: LocalDate? = null, releaseTypeFilter: String? = null, + floor: String? = null, ): List { val today = LocalDate.now() val params = mutableMapOf() @@ -1709,6 +1759,11 @@ return MessageResponse( if (rtNorm == "isExtra") { sqlBuilder.append(" AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' ") } + // Optional floor: used for Truck X (null storeId) display split; does not write storeId. + val floorClause = truckXPreferredFloorExistsSql(floor) + if (floorClause.isNotBlank()) { + sqlBuilder.append(" ").append(floorClause).append(" ") + } sqlBuilder.append(" ORDER BY dop.requiredDeliveryDate, dop.truckDepartureTime, dop.truckLanceCode, dop.id ") val rows: List> = try { jdbcDao.queryForList(sqlBuilder.toString(), params) diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt index a557e76..c16ee15 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt @@ -94,6 +94,7 @@ open class DoWorkbenchReleaseService( private val jdbcDao: JdbcDao, private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService, private val stockOutLineWorkbenchService: StockOutLineWorkbenchService, + private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, ) { private val poolSize = Runtime.getRuntime().availableProcessors() private val executor = Executors.newFixedThreadPool(kotlin.math.min(poolSize, 4)) @@ -523,7 +524,9 @@ open class DoWorkbenchReleaseService( params: MutableMap, ) { if (storeId.isNullOrBlank()) { + // Truck X: storeId stays null, but do not merge across supplier preferred floors (2F vs 4F). sql.append(" AND (storeId IS NULL OR TRIM(COALESCE(storeId, '')) = '') ") + appendTruckXPreferredFloorMatchClauses(sql, first.preferredFloor) } else { sql.append(" AND storeId = :storeId ") params["storeId"] = storeId @@ -552,6 +555,59 @@ open class DoWorkbenchReleaseService( } } + /** + * For Truck X headers (`storeId` null): only merge into a ticket whose linked DO suppliers + * all resolve to the same preferred floor as [preferredFloor] (4F list wins; else 2F), + * matching normal lane separation by floor / [resolveStoreId]. + */ + private fun appendTruckXPreferredFloorMatchClauses( + sql: StringBuilder, + preferredFloor: String?, + ) { + val floor = when (preferredFloor?.trim()?.uppercase(java.util.Locale.ROOT)?.replace("/", "")) { + "4F" -> "4F" + else -> "2F" + } + val (_, s4f) = doFloorSupplierSettingsService.loadDoFloorSupplierLists() + val in4 = s4f.joinToString(", ") { "'" + it.replace("'", "''") + "'" } + .ifEmpty { "'__NO_4F_SUPPLIER__'" } + val floorExpr = "CASE WHEN s.code IN ($in4) THEN '4F' ELSE '2F' END" + // Reject tickets that already have any DO on a different floor. + sql.append( + """ + AND NOT EXISTS ( + SELECT 1 + FROM fpsmsdb.pick_order po + INNER JOIN fpsmsdb.delivery_order d ON d.id = po.doId AND d.deleted = 0 + LEFT JOIN fpsmsdb.shop s ON s.id = d.supplierId AND s.deleted = 0 + WHERE po.deliveryOrderPickOrderId = delivery_order_pick_order.id + AND po.deleted = 0 + AND ($floorExpr) <> '$floor' + ) + """.trimIndent() + ) + // Prefer an existing same-floor ticket; allow empty header (no pick_order yet). + sql.append( + """ + AND ( + NOT EXISTS ( + SELECT 1 FROM fpsmsdb.pick_order po0 + WHERE po0.deliveryOrderPickOrderId = delivery_order_pick_order.id AND po0.deleted = 0 + ) + OR EXISTS ( + SELECT 1 + FROM fpsmsdb.pick_order po + INNER JOIN fpsmsdb.delivery_order d ON d.id = po.doId AND d.deleted = 0 + LEFT JOIN fpsmsdb.shop s ON s.id = d.supplierId AND s.deleted = 0 + WHERE po.deliveryOrderPickOrderId = delivery_order_pick_order.id + AND po.deleted = 0 + AND ($floorExpr) = '$floor' + ) + ) + """.trimIndent() + ) + } + private fun mapRowToMergeableTicket(row: Map): MergeableWorkbenchTicket? { fun cell(name: String): String? { val key = row.keys.find { it.equals(name, true) } ?: return null diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt index 702db0a..9ffbd45 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DoWorkbenchController.kt @@ -115,12 +115,15 @@ class DoWorkbenchController( @RequestParam(required = false) storeId: String?, @RequestParam(required = false) truck: String?, @RequestParam(required = false) releaseType: String?, + /** Optional `2F`/`4F`: Truck X list split by DO supplier preferred floor (storeId stays null). */ + @RequestParam(required = false) floor: String?, ): List { return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelection( shopName, storeId, truck, releaseTypeFilter = releaseType, + floor = floor, ) } @@ -131,6 +134,8 @@ class DoWorkbenchController( @RequestParam(required = false) truck: String?, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?, @RequestParam(required = false) releaseType: String?, + /** Optional `2F`/`4F`: Truck X list split by DO supplier preferred floor (storeId stays null). */ + @RequestParam(required = false) floor: String?, ): List { return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( shopName, @@ -138,6 +143,7 @@ class DoWorkbenchController( truck, requiredDeliveryDate = requiredDate, releaseTypeFilter = releaseType, + floor = floor, ) }