| @@ -16,6 +16,8 @@ import java.time.LocalDate | |||||
| @Repository | @Repository | ||||
| interface DoPickOrderRecordRepository : JpaRepository<DoPickOrderRecord, Long> { | interface DoPickOrderRecordRepository : JpaRepository<DoPickOrderRecord, Long> { | ||||
| fun findByPickOrderId(pickOrderId: Long): List<DoPickOrderRecord> | fun findByPickOrderId(pickOrderId: Long): List<DoPickOrderRecord> | ||||
| fun findByDoOrderIdAndDeletedFalse(doOrderId: Long): List<DoPickOrderRecord> | |||||
| fun findByTicketNoStartingWith(ticketPrefix: String): List<DoPickOrderRecord> | fun findByTicketNoStartingWith(ticketPrefix: String): List<DoPickOrderRecord> | ||||
| @Query(""" | @Query(""" | ||||
| @@ -21,6 +21,8 @@ interface DoPickOrderRepository : JpaRepository<DoPickOrder, Long> { | |||||
| ): List<DoPickOrder> | ): List<DoPickOrder> | ||||
| fun findByPickOrderId(pickOrderId: Long): List<DoPickOrder> | fun findByPickOrderId(pickOrderId: Long): List<DoPickOrder> | ||||
| fun findByDoOrderIdAndDeletedFalse(doOrderId: Long): List<DoPickOrder> | |||||
| fun findByTicketStatusIn(statuses: List<DoPickOrderStatus>): List<DoPickOrder> | fun findByTicketStatusIn(statuses: List<DoPickOrderStatus>): List<DoPickOrder> | ||||
| // 在 DoPickOrderRepository 中添加这个方法 | // 在 DoPickOrderRepository 中添加这个方法 | ||||
| fun findByHandledByAndTicketStatusIn(handledBy: Long, status: List<DoPickOrderStatus>): List<DoPickOrder> | fun findByHandledByAndTicketStatusIn(handledBy: Long, status: List<DoPickOrderStatus>): List<DoPickOrder> | ||||
| @@ -473,8 +473,66 @@ open class DeliveryOrderService( | |||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * Picker display name from [DeliveryOrderPickOrder.handlerName] (workbench source of truth). | |||||
| * Linked via [pick_order.doId] or [do_pick_order_line.do_order_id]; falls back to [User.name] | |||||
| * from [DeliveryOrderPickOrder.handledBy] when [handlerName] was not persisted. | |||||
| */ | |||||
| private fun resolveHandlerNameForDeliveryOrder(doId: Long): String? { | |||||
| val sql = """ | |||||
| SELECT DISTINCT TRIM(handler_name) AS handler_name | |||||
| FROM ( | |||||
| SELECT COALESCE( | |||||
| NULLIF(TRIM(dop.handlerName), ''), | |||||
| NULLIF(TRIM(u.name), '') | |||||
| ) AS handler_name | |||||
| FROM fpsmsdb.delivery_order_pick_order dop | |||||
| INNER JOIN fpsmsdb.pick_order po | |||||
| ON po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0 | |||||
| LEFT JOIN fpsmsdb.user u | |||||
| ON u.id = dop.handledBy AND u.deleted = 0 | |||||
| WHERE dop.deleted = 0 | |||||
| AND po.doId = :doId | |||||
| UNION | |||||
| SELECT COALESCE( | |||||
| NULLIF(TRIM(dop.handlerName), ''), | |||||
| NULLIF(TRIM(u.name), '') | |||||
| ) AS handler_name | |||||
| FROM fpsmsdb.delivery_order_pick_order dop | |||||
| INNER JOIN fpsmsdb.pick_order po | |||||
| ON po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0 | |||||
| INNER JOIN fpsmsdb.do_pick_order_line dpol | |||||
| ON dpol.pick_order_id = po.id AND dpol.deleted = 0 | |||||
| LEFT JOIN fpsmsdb.user u | |||||
| ON u.id = dop.handledBy AND u.deleted = 0 | |||||
| WHERE dop.deleted = 0 | |||||
| AND dpol.do_order_id = :doId | |||||
| ) resolved | |||||
| WHERE handler_name IS NOT NULL AND TRIM(handler_name) <> '' | |||||
| ORDER BY handler_name | |||||
| """.trimIndent() | |||||
| val names = jdbcDao.queryForList(sql, mapOf("doId" to doId)) | |||||
| .mapNotNull { row -> | |||||
| val key = row.keys.find { it.equals("handler_name", true) } ?: return@mapNotNull null | |||||
| row[key]?.toString()?.trim()?.takeIf { it.isNotEmpty() } | |||||
| } | |||||
| .distinct() | |||||
| .sorted() | |||||
| return names.joinToString(",").ifBlank { null } | |||||
| } | |||||
| open fun getDetailedDo(id: Long): DoDetailResponse? { | open fun getDetailedDo(id: Long): DoDetailResponse? { | ||||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(id) ?: return null | val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(id) ?: return null | ||||
| val handlerName = try { | |||||
| resolveHandlerNameForDeliveryOrder(id) | |||||
| } catch (ex: Exception) { | |||||
| log.warn("Failed to resolve handler name for delivery order {}: {}", id, ex.message) | |||||
| null | |||||
| } | |||||
| val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | val itemIds = deliveryOrder.deliveryOrderLines.mapNotNull { it.item?.id }.distinct() | ||||
| val stockQtyByItemId = itemIds.associateWith { itemId -> | val stockQtyByItemId = itemIds.associateWith { itemId -> | ||||
| @@ -493,6 +551,7 @@ open class DeliveryOrderService( | |||||
| completeDate = deliveryOrder.completeDate, | completeDate = deliveryOrder.completeDate, | ||||
| status = deliveryOrder.status?.value, | status = deliveryOrder.status?.value, | ||||
| isExtra = deliveryOrder.isExtra, | isExtra = deliveryOrder.isExtra, | ||||
| handlerName = handlerName, | |||||
| deliveryOrderLines = deliveryOrder.deliveryOrderLines.map { line -> | deliveryOrderLines = deliveryOrder.deliveryOrderLines.map { line -> | ||||
| val itemId = line.item?.id | val itemId = line.item?.id | ||||
| val stockQty = itemId?.let { stockQtyByItemId[it] } ?: BigDecimal.ZERO | val stockQty = itemId?.let { stockQtyByItemId[it] } ?: BigDecimal.ZERO | ||||
| @@ -20,6 +20,8 @@ data class DoDetailResponse( | |||||
| val status: String?, | val status: String?, | ||||
| /** 加單 DO(M18 加單專用同步) */ | /** 加單 DO(M18 加單專用同步) */ | ||||
| val isExtra: Boolean = false, | val isExtra: Boolean = false, | ||||
| /** 揀貨員名稱(來源:delivery_order_pick_order.handlerName) */ | |||||
| val handlerName: String? = null, | |||||
| val deliveryOrderLines: List<DoDetailLineResponse> | val deliveryOrderLines: List<DoDetailLineResponse> | ||||
| ) | ) | ||||
| @@ -0,0 +1,88 @@ | |||||
| -- liquibase formatted sql | |||||
| -- changeset KelvinY:20260611_01_create_do_replenishment | |||||
| -- preconditions onFail:MARK_RAN | |||||
| -- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'do_replenishment' | |||||
| CREATE TABLE IF NOT EXISTS `do_replenishment` | |||||
| ( | |||||
| `id` BIGINT NOT NULL AUTO_INCREMENT, | |||||
| `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
| `createdBy` VARCHAR(30) NULL DEFAULT NULL, | |||||
| `version` INT NOT NULL DEFAULT '0', | |||||
| `modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, | |||||
| `modifiedBy` VARCHAR(30) NULL DEFAULT NULL, | |||||
| `deleted` TINYINT(1) NOT NULL DEFAULT '0', | |||||
| `code` VARCHAR(100) NOT NULL, | |||||
| `deliveryDate` DATE NOT NULL, | |||||
| `sourceDoId` BIGINT NOT NULL, | |||||
| `sourceDoCode` VARCHAR(100) NULL DEFAULT NULL, | |||||
| `sourceDoLineId` BIGINT NOT NULL, | |||||
| `sourceM18DataLogId` BIGINT NOT NULL, | |||||
| `sourceM18Id` BIGINT NOT NULL, | |||||
| `itemId` BIGINT NOT NULL, | |||||
| `itemNo` VARCHAR(100) NULL DEFAULT NULL, | |||||
| `itemName` VARCHAR(255) NULL DEFAULT NULL, | |||||
| `replenishQty` DECIMAL(14, 2) NOT NULL, | |||||
| `uomId` BIGINT NULL DEFAULT NULL, | |||||
| `shopId` BIGINT NULL DEFAULT NULL, | |||||
| `shopCode` VARCHAR(50) NULL DEFAULT NULL, | |||||
| `shopName` VARCHAR(255) NULL DEFAULT NULL, | |||||
| `truckLaneCode` VARCHAR(100) NULL DEFAULT NULL, | |||||
| `targetDoId` BIGINT NULL DEFAULT NULL, | |||||
| `targetDoCode` VARCHAR(100) NULL DEFAULT NULL, | |||||
| `pickOrderLineId` BIGINT NULL DEFAULT NULL, | |||||
| `status` VARCHAR(20) NOT NULL DEFAULT 'pending', | |||||
| CONSTRAINT pk_do_replenishment PRIMARY KEY (`id`) | |||||
| ); | |||||
| SET @idx_dr_code := ( | |||||
| SELECT COUNT(*) FROM information_schema.statistics | |||||
| WHERE table_schema = DATABASE() AND table_name = 'do_replenishment' AND index_name = 'uk_dr_code' | |||||
| ); | |||||
| SET @sql_dr_code := IF( | |||||
| @idx_dr_code = 0, | |||||
| 'CREATE UNIQUE INDEX uk_dr_code ON `do_replenishment` (`code`)', | |||||
| 'SELECT 1' | |||||
| ); | |||||
| PREPARE stmt_dr_code FROM @sql_dr_code; | |||||
| EXECUTE stmt_dr_code; | |||||
| DEALLOCATE PREPARE stmt_dr_code; | |||||
| SET @idx_dr_status_date := ( | |||||
| SELECT COUNT(*) FROM information_schema.statistics | |||||
| WHERE table_schema = DATABASE() AND table_name = 'do_replenishment' AND index_name = 'idx_dr_status_delivery_date' | |||||
| ); | |||||
| SET @sql_dr_sd := IF( | |||||
| @idx_dr_status_date = 0, | |||||
| 'CREATE INDEX idx_dr_status_delivery_date ON `do_replenishment` (`status`, `deliveryDate`)', | |||||
| 'SELECT 1' | |||||
| ); | |||||
| PREPARE stmt_dr_sd FROM @sql_dr_sd; | |||||
| EXECUTE stmt_dr_sd; | |||||
| DEALLOCATE PREPARE stmt_dr_sd; | |||||
| SET @idx_dr_lane_shop := ( | |||||
| SELECT COUNT(*) FROM information_schema.statistics | |||||
| WHERE table_schema = DATABASE() AND table_name = 'do_replenishment' AND index_name = 'idx_dr_lane_shop_status' | |||||
| ); | |||||
| SET @sql_dr_ls := IF( | |||||
| @idx_dr_lane_shop = 0, | |||||
| 'CREATE INDEX idx_dr_lane_shop_status ON `do_replenishment` (`truckLaneCode`, `shopId`, `status`)', | |||||
| 'SELECT 1' | |||||
| ); | |||||
| PREPARE stmt_dr_ls FROM @sql_dr_ls; | |||||
| EXECUTE stmt_dr_ls; | |||||
| DEALLOCATE PREPARE stmt_dr_ls; | |||||
| SET @idx_dr_source_line := ( | |||||
| SELECT COUNT(*) FROM information_schema.statistics | |||||
| WHERE table_schema = DATABASE() AND table_name = 'do_replenishment' AND index_name = 'idx_dr_source_line' | |||||
| ); | |||||
| SET @sql_dr_sl := IF( | |||||
| @idx_dr_source_line = 0, | |||||
| 'CREATE INDEX idx_dr_source_line ON `do_replenishment` (`sourceDoId`, `sourceDoLineId`)', | |||||
| 'SELECT 1' | |||||
| ); | |||||
| PREPARE stmt_dr_sl FROM @sql_dr_sl; | |||||
| EXECUTE stmt_dr_sl; | |||||
| DEALLOCATE PREPARE stmt_dr_sl; | |||||