From 870fbca20e62de811a560111a2b5ee4fd4d96e64 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Thu, 14 May 2026 15:04:54 +0800 Subject: [PATCH] new supplier isEtra new do chart do saerch batch release button put down not lot requied qty show 0 fix --- .../m18/service/M18DeliveryOrderService.kt | 12 +- .../ffii/fpsms/m18/web/M18TestController.kt | 6 +- .../modules/chart/service/ChartService.kt | 46 ++-- .../modules/chart/web/ChartController.kt | 10 +- .../deliveryOrder/entity/DeliveryOrder.kt | 4 +- .../entity/DeliveryOrderRepository.kt | 8 +- .../entity/models/DeliveryOrderInfo.kt | 6 +- .../service/DeliveryOrderService.kt | 134 +++++------ .../service/DoFloorSupplierSettingsService.kt | 95 ++++++++ .../service/DoReleaseCoordinatorService.kt | 27 +-- .../DoWorkbenchDopoAssignmentService.kt | 11 + .../service/DoWorkbenchMainService.kt | 216 +++++++++++++++++- .../service/DoWorkbenchReleaseService.kt | 29 ++- .../web/DeliveryOrderController.kt | 6 +- .../web/DoWorkbenchController.kt | 19 +- .../web/models/DoDetailResponse.kt | 19 +- .../web/models/ReleaseDoRequest.kt | 9 +- .../web/models/SaveDeliveryOrderRequest.kt | 2 +- .../service/HierarchicalFgPayloadAssembler.kt | 3 + .../modules/report/service/ReportService.kt | 22 +- .../settings/web/SettingsController.java | 16 +- .../stock/service/SuggestedPickLotService.kt | 16 +- .../CreateStockTakeForSectionsRequest.kt | 8 + .../changes/20260514_Enson/01_setting.sql | 18 ++ .../changes/20260514_Enson/02_setting.sql | 6 + 25 files changed, 552 insertions(+), 196 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt create mode 100644 src/main/resources/db/changelog/changes/20260514_Enson/01_setting.sql create mode 100644 src/main/resources/db/changelog/changes/20260514_Enson/02_setting.sql diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt index 52c2576..08781e3 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt @@ -154,20 +154,20 @@ open class M18DeliveryOrderService( open fun saveDeliveryOrders(request: M18CommonRequest): SyncResult { val deliveryOrdersWithType = getDeliveryOrdersWithType(request) - return saveDeliveryOrdersWithPreparedList(deliveryOrdersWithType, syncIsEtra = false) + return saveDeliveryOrdersWithPreparedList(deliveryOrdersWithType, syncisExtra = false) } /** * Sync a single M18 shop PO / delivery order by document [code], same search pattern as * [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode]. * - * @param isEtraSync when true, persist local `delivery_order.isEtra=true` (manual DO(加單) sync). + * @param isExtraSync when true, persist local `delivery_order.isExtra=true` (manual DO(加單) sync). * No M18-side "加單" filtering is used. * @param newOnly when true, skip if a non-deleted local DO already exists with the same `code`. */ open fun saveDeliveryOrderByCode( code: String, - isEtraSync: Boolean = false, + isExtraSync: Boolean = false, newOnly: Boolean = false, ): SyncResult { if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(code)) { @@ -210,12 +210,12 @@ open class M18DeliveryOrderService( query = conds ) - return saveDeliveryOrdersWithPreparedList(prepared, syncIsEtra = isEtraSync) + return saveDeliveryOrdersWithPreparedList(prepared, syncisExtra = isExtraSync) } private fun saveDeliveryOrdersWithPreparedList( deliveryOrdersWithType: M18PurchaseOrderListResponseWithType?, - syncIsEtra: Boolean = false, + syncisExtra: Boolean = false, ): SyncResult { logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------") @@ -303,7 +303,7 @@ open class M18DeliveryOrderService( handlerId = null, m18BeId = mainpo.beId, deleted = mainpo.udfIsVoid == true, - isEtra = syncIsEtra, + isExtra = syncisExtra, ) val saveDeliveryOrderResponse = diff --git a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt index 4da2df9..2138251 100644 --- a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt +++ b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt @@ -82,14 +82,14 @@ class M18TestController ( @GetMapping("/test/do-by-code") fun testSyncDoByCode(@RequestParam code: String): SyncResult { - return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isEtraSync = false) + return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isExtraSync = false) } - /** DO(加單):手動按 code 同步,並寫入本地 [DeliveryOrder.isEtra]=true(不做 M18 端加單條件過濾) */ + /** DO(加單):手動按 code 同步,並寫入本地 [DeliveryOrder.isExtra]=true(不做 M18 端加單條件過濾) */ @GetMapping("/test/do-by-code-extra") fun testSyncDoByCodeExtra(@RequestParam code: String): SyncResult { // 加單 tab: only sync when it's a NEW order (not existing in local system) - return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isEtraSync = true, newOnly = true) + return m18DeliveryOrderService.saveDeliveryOrderByCode(code, isExtraSync = true, newOnly = true) } @GetMapping("/test/product-by-code") diff --git a/src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt b/src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt index 73b3a2e..90b6f5d 100644 --- a/src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt +++ b/src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt @@ -721,23 +721,27 @@ open class ChartService( /** * Staff delivery performance: daily pick ticket count and total time per staff. - * Uses do_pick_order_record (handler = handledBy); time = sum of (ticketCompleteDateTime - ticketReleaseTime) per record. - * Optionally use do_pick_order_line_record for line count; here orderCount = number of completed pick tickets. + * Uses delivery_order_pick_order (handler = handledBy); time = sum of + * (ticketCompleteDateTime - ticketReleaseTime) per completed ticket. * staffNos: when non-empty, filter to these staff by user.staffNo (multi-select). + * storeIdNull: when true, only rows with dop.storeId IS NULL (takes precedence over storeId). + * storeId: when non-blank and storeIdNull is not true, filter dop.storeId equality (trimmed). */ fun getStaffDeliveryPerformance( startDate: LocalDate?, endDate: LocalDate?, - staffNos: List? + staffNos: List?, + storeId: String?, + storeIdNull: Boolean?, ): List> { val args = mutableMapOf() val startSql = if (startDate != null) { args["startDate"] = startDate.toString() - "AND DATE(dpor.ticketCompleteDateTime) >= :startDate" + "AND DATE(dop.ticketCompleteDateTime) >= :startDate" } else "" val endSql = if (endDate != null) { args["endDate"] = endDate.toString() - "AND DATE(dpor.ticketCompleteDateTime) <= :endDate" + "AND DATE(dop.ticketCompleteDateTime) <= :endDate" } else "" val staffSql = if (!staffNos.isNullOrEmpty()) { val nos = staffNos.map { it.trim() }.filter { it.isNotBlank() } @@ -746,25 +750,33 @@ open class ChartService( "AND u.staffNo IN (:staffNos)" } } else "" + val storeSql = when { + storeIdNull == true -> "AND dop.storeId IS NULL" + !storeId.isNullOrBlank() -> { + args["filterStoreId"] = storeId.trim() + "AND dop.storeId = :filterStoreId" + } + else -> "" + } val sql = """ SELECT - DATE_FORMAT(dpor.ticketCompleteDateTime, '%Y-%m-%d') AS date, - COALESCE(u.name, dpor.handler_name, 'Unknown') AS staffName, - COUNT(dpor.id) AS orderCount, + DATE_FORMAT(dop.ticketCompleteDateTime, '%Y-%m-%d') AS date, + COALESCE(NULLIF(TRIM(COALESCE(u.name, '')), ''), dop.handlerName, 'Unknown') AS staffName, + COUNT(dop.id) AS orderCount, COALESCE(SUM( CASE - WHEN dpor.ticket_release_time IS NOT NULL AND dpor.ticketCompleteDateTime IS NOT NULL - THEN GREATEST(0, TIMESTAMPDIFF(MINUTE, dpor.ticket_release_time, dpor.ticketCompleteDateTime)) + WHEN dop.ticketReleaseTime IS NOT NULL AND dop.ticketCompleteDateTime IS NOT NULL + THEN GREATEST(0, TIMESTAMPDIFF(MINUTE, dop.ticketReleaseTime, dop.ticketCompleteDateTime)) ELSE 0 END ), 0) AS totalMinutes - FROM do_pick_order_record dpor - LEFT JOIN user u ON dpor.handled_by = u.id AND u.deleted = 0 - WHERE dpor.deleted = 0 - AND dpor.ticket_status = 'completed' - AND dpor.ticketCompleteDateTime IS NOT NULL - $startSql $endSql $staffSql - GROUP BY DATE(dpor.ticketCompleteDateTime), dpor.handled_by, u.name, dpor.handler_name + FROM delivery_order_pick_order dop + LEFT JOIN user u ON dop.handledBy = u.id AND u.deleted = 0 + WHERE dop.deleted = 0 + AND LOWER(COALESCE(dop.ticketStatus, '')) = 'completed' + AND dop.ticketCompleteDateTime IS NOT NULL + $startSql $endSql $staffSql $storeSql + GROUP BY DATE(dop.ticketCompleteDateTime), dop.handledBy, u.name, dop.handlerName ORDER BY date, orderCount DESC """.trimIndent() return jdbcDao.queryForList(sql, args) diff --git a/src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt b/src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt index c83ef36..3de7d68 100644 --- a/src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt +++ b/src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt @@ -192,16 +192,20 @@ class ChartController( chartService.getStaffDeliveryPerformanceHandlers() /** - * GET /chart/staff-delivery-performance?startDate=&endDate=&staffNo=A001&staffNo=A002 - * Returns [{ date, staffName, orderCount, totalMinutes }]. Data from do_pick_order_record (handled_by), orderCount = completed pick tickets, totalMinutes = sum(ticketCompleteDateTime - ticketReleaseTime). + * GET /chart/staff-delivery-performance?startDate=&endDate=&staffNo=A001&staffNo=A002&storeId=2/F&storeIdNull=true + * Returns [{ date, staffName, orderCount, totalMinutes }]. Data from delivery_order_pick_order + * (handledBy), orderCount = completed pick tickets, totalMinutes = sum(ticketCompleteDateTime - ticketReleaseTime). + * Optional storeId filters delivery_order_pick_order.storeId; storeIdNull=true means IS NULL (overrides storeId). */ @GetMapping("/staff-delivery-performance") fun getStaffDeliveryPerformance( @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?, @RequestParam(required = false) staffNo: List?, + @RequestParam(required = false) storeId: String?, + @RequestParam(required = false) storeIdNull: Boolean?, ): List> = - chartService.getStaffDeliveryPerformance(startDate, endDate, staffNo) + chartService.getStaffDeliveryPerformance(startDate, endDate, staffNo, storeId, storeIdNull) // ---------- Job order reports ---------- diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrder.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrder.kt index d9dc6f2..1e61124 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrder.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrder.kt @@ -64,6 +64,6 @@ open class DeliveryOrder: BaseEntity() { open var m18BeId: Long? = null /** 加單:由 M18「加單」專用同步標記;一般 DO 為 false */ - @Column(name = "isEtra", nullable = false) - open var isEtra: Boolean = false + @Column(name = "isExtra", nullable = false) + open var isExtra: Boolean = false } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderRepository.kt index 51c9260..65b48fd 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderRepository.kt @@ -111,7 +111,7 @@ fun searchDoLite( and (:status is null or d.status = :status) and (:etaStart is null or d.estimatedArrivalDate >= :etaStart) and (:etaEnd is null or d.estimatedArrivalDate < :etaEnd) - and (:isEtra is null or d.isEtra = :isEtra) + and (:isExtra is null or d.isExtra = :isExtra) order by d.id desc """) fun searchDoLitePage( @@ -120,7 +120,7 @@ fun searchDoLitePage( @Param("status") status: DeliveryOrderStatus?, @Param("etaStart") etaStart: LocalDateTime?, @Param("etaEnd") etaEnd: LocalDateTime?, - @Param("isEtra") isEtra: Boolean?, + @Param("isExtra") isExtra: Boolean?, pageable: Pageable ): Page @@ -136,7 +136,7 @@ fun searchDoLitePage( and (:status is null or d.status = :status) and (:etaStart is null or d.estimatedArrivalDate >= :etaStart) and (:etaEnd is null or d.estimatedArrivalDate < :etaEnd) - and (:isEtra is null or d.isEtra = :isEtra) + and (:isExtra is null or d.isExtra = :isExtra) and d.supplier is not null and d.supplier.code in :allowedSupplierCodes order by d.id desc @@ -148,7 +148,7 @@ fun searchDoLitePageWithSupplierCodes( @Param("status") status: DeliveryOrderStatus?, @Param("etaStart") etaStart: LocalDateTime?, @Param("etaEnd") etaEnd: LocalDateTime?, - @Param("isEtra") isEtra: Boolean?, + @Param("isExtra") isExtra: Boolean?, @Param("allowedSupplierCodes") allowedSupplierCodes: List, pageable: Pageable, ): Page diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt index f806cb0..c27646e 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/models/DeliveryOrderInfo.kt @@ -48,8 +48,8 @@ interface DeliveryOrderInfoLite { @get:Value("#{target.shop?.addr3}") val shopAddress: String? - @get:Value("#{target.isEtra}") - val isEtra: Boolean + @get:Value("#{target.isExtra}") + val isExtra: Boolean } data class DeliveryOrderInfoLiteDto( val id: Long, @@ -61,5 +61,5 @@ data class DeliveryOrderInfoLiteDto( val supplierName: String?, val shopAddress: String?, val truckLanceCode: String?, - val isEtra: Boolean = false, + val isExtra: Boolean = false, ) \ No newline at end of file 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 3f7ef62..abfba71 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 @@ -90,7 +90,6 @@ import com.ffii.fpsms.modules.stock.entity.InventoryLotLine import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo import java.util.Locale import org.slf4j.Logger - @Service open class DeliveryOrderService( private val deliveryOrderRepository: DeliveryOrderRepository, @@ -121,23 +120,23 @@ open class DeliveryOrderService( private val doPickOrderLineRepository: DoPickOrderLineRepository, private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, private val itemsRepository: ItemsRepository, + private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, ) { /** - * 樓層篩選:2F → P07/P06D(車線- 族);4F → P06B(P06B_ 族);全部 → 三者。 - * 車線-X 仍屬該 DO 的 supplier,故 P06B+車線-X 不會出現在 2F,P06D+車線-X 不會出現在 4F。 + * 樓層篩選:2F/4F/ALL 由 [DoFloorSupplierSettingsService] 讀 `settings`。 + * 車線-X 仍依 DO supplier 所屬樓層出現在對應 tab。 */ - private fun allowedSupplierCodesForFloor(floor: String?): List { - val f = floor?.trim()?.uppercase(Locale.ROOT).orEmpty() - if (f.isEmpty() || f == "ALL" || f == "All") { - return listOf("P06B", "P07", "P06D") - } - return when (f) { - "2F" -> listOf("P07", "P06D") - "4F" -> listOf("P06B") - else -> listOf("P06B", "P07", "P06D") - } - } + private fun allowedSupplierCodesForFloor(floor: String?): List = + doFloorSupplierSettingsService.allowedSupplierCodesForFloor(floor) + + private fun loadDoFloorSupplierLists(): Pair, List> = + doFloorSupplierSettingsService.loadDoFloorSupplierLists() + private fun preferredStoreFloorForSupplier( + supplierCode: String?, + suppliers2F: List, + suppliers4F: List, + ): String = doFloorSupplierSettingsService.preferredStoreFloorForSupplier(supplierCode, suppliers2F, suppliers4F) open fun searchDoLiteByPage( code: String?, shopName: String?, @@ -147,7 +146,7 @@ open class DeliveryOrderService( pageSize: Int?, truckLanceCode: String?, floor: String? = null, - isEtra: Boolean? = null, + isExtra: Boolean? = null, ): RecordsRes { val page = (pageNum ?: 1) - 1 @@ -169,7 +168,7 @@ open class DeliveryOrderService( status = statusEnum, etaStart = etaStart, etaEnd = etaEnd, - isEtra = isEtra, + isExtra = isExtra, allowedSupplierCodes = allowedForFloor, pageable = PageRequest.of(0, 100_000), ) @@ -181,6 +180,7 @@ open class DeliveryOrderService( .associateBy { it.id } val preFilteredContent = allResult.content + val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() // ✅ 优化3: 收集所有需要查询的 shopId 和日期组合(只处理预过滤后的记录) val shopIdAndDatePairs = preFilteredContent.mapNotNull { info -> @@ -191,11 +191,7 @@ open class DeliveryOrderService( val targetDate = estimatedArrivalDate.toLocalDate() val dayAbbr = getDayOfWeekAbbr(targetDate) val supplierCode = deliveryOrder.supplier?.code - val preferredFloor = when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> "2F" // 或者改成 null / 其他默认值,看你业务需要 - } + val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) Triple(shopId, preferredFloor, dayAbbr) } else { null @@ -217,11 +213,7 @@ open class DeliveryOrderService( val processedRecords = preFilteredContent.map { info -> val deliveryOrder = deliveryOrdersMap[info.id] val supplierCode = deliveryOrder?.supplier?.code - val preferredFloor = when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> "2F" - } + val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) val shop = deliveryOrder?.shop val shopId = shop?.id val estimatedArrivalDate = info.estimatedArrivalDate @@ -248,7 +240,7 @@ open class DeliveryOrderService( supplierName = info.supplierName, shopAddress = info.shopAddress, truckLanceCode = calculatedTruckLanceCode, - isEtra = deliveryOrdersMap[info.id]?.isEtra ?: info.isEtra, + isExtra = deliveryOrdersMap[info.id]?.isExtra ?: info.isExtra, ) }.filter { dto -> val dtoTruckLanceCode = dto.truckLanceCode?.lowercase() ?: "" @@ -279,19 +271,16 @@ open class DeliveryOrderService( status = statusEnum, etaStart = etaStart, etaEnd = etaEnd, - isEtra = isEtra, + isExtra = isExtra, allowedSupplierCodes = allowedSupplierCodes, pageable = PageRequest.of(page.coerceAtLeast(0), size), ) + val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() val records = result.content.map { info -> val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(info.id) val supplierCode = deliveryOrder?.supplier?.code - val preferredFloor = when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> "2F" - } + val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) val shop = deliveryOrder?.shop val shopId = shop?.id val estimatedArrivalDate = info.estimatedArrivalDate @@ -315,7 +304,7 @@ open class DeliveryOrderService( supplierName = info.supplierName, shopAddress = info.shopAddress, truckLanceCode = calculatedTruckLanceCode, - isEtra = deliveryOrder?.isEtra ?: info.isEtra, + isExtra = deliveryOrder?.isExtra ?: info.isExtra, ) } @@ -338,7 +327,7 @@ open class DeliveryOrderService( pageSize: Int?, truckLanceCode: String?, floor: String? = null, - isEtra: Boolean? = null, + isExtra: Boolean? = null, ): RecordsRes { val mode = TruckLaneSearchSpec.parse(truckLanceCode) if (mode is TruckLaneSearchSpec.Mode.NoFilter) { @@ -351,7 +340,7 @@ open class DeliveryOrderService( pageSize, null, floor, - isEtra, + isExtra, ) } val pageIdx = (pageNum ?: 1).coerceAtLeast(1) - 1 @@ -367,7 +356,7 @@ open class DeliveryOrderService( statusEnum = statusEnum, etaStart = etaStart, etaEnd = etaEnd, - isEtra = isEtra, + isExtra = isExtra, allowedSupplierCodes = allowedSupplierCodesForFloor(floor), lanePredicate = lanePredicate, ) @@ -391,7 +380,7 @@ open class DeliveryOrderService( pageNum: Int?, pageSize: Int?, floor: String? = null, - isEtra: Boolean? = null, + isExtra: Boolean? = null, ): RecordsRes { val page = (pageNum ?: 1) - 1 val size = pageSize ?: 10 @@ -406,22 +395,19 @@ open class DeliveryOrderService( status = statusEnum, etaStart = etaStart, etaEnd = etaEnd, - isEtra = isEtra, + isExtra = isExtra, allowedSupplierCodes = allowedSupplierCodes, pageable = PageRequest.of(0, 100_000), ) val deliveryOrderIds = allResult.content.mapNotNull { it.id } val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds).associateBy { it.id } + val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() val processedRecords = allResult.content.map { info -> val deliveryOrder = deliveryOrdersMap[info.id] val supplierCode = deliveryOrder?.supplier?.code - val preferredFloor = when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> "2F" - } + val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) val shop = deliveryOrder?.shop val shopId = shop?.id val infoEta = info.estimatedArrivalDate @@ -445,7 +431,7 @@ open class DeliveryOrderService( supplierName = info.supplierName, shopAddress = info.shopAddress, truckLanceCode = calculatedTruckLanceCode, - isEtra = deliveryOrdersMap[info.id]?.isEtra ?: info.isEtra, + isExtra = deliveryOrdersMap[info.id]?.isExtra ?: info.isExtra, ) }.filter { dto -> TruckLaneSearchSpec.isUnassignedResolvedLane(dto.truckLanceCode) } @@ -487,7 +473,7 @@ open class DeliveryOrderService( estimatedArrivalDate = deliveryOrder.estimatedArrivalDate, completeDate = deliveryOrder.completeDate, status = deliveryOrder.status?.value, - isEtra = deliveryOrder.isEtra, + isExtra = deliveryOrder.isExtra, deliveryOrderLines = deliveryOrder.deliveryOrderLines.map { line -> DoDetailLineResponse( id = line.id!!, @@ -808,7 +794,7 @@ open class DeliveryOrderService( this.handler = handler m18BeId = request.m18BeId this.deleted = request.deleted - isEtra = request.isEtra ?: false + isExtra = request.isExtra ?: false } val savedDeliveryOrder = deliveryOrderRepository.saveAndFlush(deliveryOrder).let { @@ -948,14 +934,10 @@ open class DeliveryOrderService( println(" DEBUG: Target date: $targetDate, Date prefix: $datePrefix") - // 新逻辑:根据 supplier code 决定楼层 - // 如果 supplier code 是 "P06B",使用 4F,否则使用 2F + // 新逻辑:根据 supplier code 决定楼层(清單來自 settings) val supplierCode = deliveryOrder.supplier?.code - val preferredFloor = when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> "2F" // 或者改成 null / 其他默认值,看你业务需要 - } + val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() + val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) println(" DEBUG: Supplier code: $supplierCode, Preferred floor: $preferredFloor") @@ -1839,15 +1821,11 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } } } - // 新逻辑:根据 supplier code 决定楼层 - // 如果 supplier code 是 "P06B",使用 4F,否则使用 2F + // 新逻辑:根据 supplier code 决定楼层(清單來自 settings) val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() val supplierCode = deliveryOrder.supplier?.code - val preferredFloor = when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> "2F" // 或者改成 null / 其他默认值,看你业务需要 - } + val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() + val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) println(" DEBUG: Floor calculation for DO ${deliveryOrder.id}") println(" - Supplier code: $supplierCode") @@ -1936,7 +1914,8 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } truckDepartureTime = effectiveTruck.departureTime, truckLanceCode = effectiveTruck.truckLanceCode, loadingSequence = effectiveTruck.loadingSequence, - usedDefaultTruck = usedDefaultTruck + usedDefaultTruck = usedDefaultTruck, + isExtra = deliveryOrder.isExtra ?: false, ) } @@ -2022,11 +2001,8 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } // Truck selection (reuse normal logic) val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() val supplierCode = deliveryOrder.supplier?.code - val preferredFloor = when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> "2F" - } + val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() + val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) val truck = deliveryOrder.shop?.id?.let { shopId -> val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) @@ -2094,7 +2070,8 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } truckDepartureTime = effectiveTruck.departureTime, truckLanceCode = effectiveTruck.truckLanceCode, loadingSequence = effectiveTruck.loadingSequence, - usedDefaultTruck = usedDefaultTruck + usedDefaultTruck = usedDefaultTruck, + isExtra = deliveryOrder.isExtra ?: false, ) } @@ -2104,7 +2081,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } statusEnum: DeliveryOrderStatus?, etaStart: LocalDateTime?, etaEnd: LocalDateTime?, - isEtra: Boolean?, + isExtra: Boolean?, allowedSupplierCodes: List, lanePredicate: (String?) -> Boolean, ): List { @@ -2118,7 +2095,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } status = statusEnum, etaStart = etaStart, etaEnd = etaEnd, - isEtra = isEtra, + isExtra = isExtra, allowedSupplierCodes = allowedSupplierCodes, pageable = PageRequest.of(dbPage, 500), ) @@ -2140,6 +2117,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } val ids = rows.mapNotNull { it.id } if (ids.isEmpty()) return emptyList() val deliveryOrdersMap = deliveryOrderRepository.findAllById(ids).associateBy { it.id } + val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() val shopIdAndDatePairs = rows.mapNotNull { info -> val d = deliveryOrdersMap[info.id] @@ -2149,11 +2127,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } val targetDate = eta.toLocalDate() val dayAbbr = getDayOfWeekAbbr(targetDate) val supplierCode = d.supplier?.code - val preferredFloor = when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> "2F" - } + val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) Triple(shopId, preferredFloor, dayAbbr) } else { null @@ -2169,11 +2143,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } return rows.map { info -> val deliveryOrder = deliveryOrdersMap[info.id] val supplierCode = deliveryOrder?.supplier?.code - val preferredFloor = when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> "2F" - } + val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) val shopId = deliveryOrder?.shop?.id val infoEta = info.estimatedArrivalDate val calculatedTruckLanceCode = @@ -2194,14 +2164,14 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } supplierName = info.supplierName, shopAddress = info.shopAddress, truckLanceCode = calculatedTruckLanceCode, - isEtra = deliveryOrder?.isEtra ?: info.isEtra, + isExtra = deliveryOrder?.isExtra ?: info.isExtra, ) } } /** * 依店鋪 + 揀貨樓層解析當日應顯示之車線。 - * - **2F**(P07/P06D):`TruckLanceCode` 多為 `車線-…` 且不含星期縮寫,不依 `LIKE '%Mon%'` 篩選;取該店該樓層未刪除車輛中出發時間最早者。 + * - **2F**(P07/P06D/P06Y):`TruckLanceCode` 多為 `車線-…` 且不含星期縮寫,不依 `LIKE '%Mon%'` 篩選;取該店該樓層未刪除車輛中出發時間最早者。 * - **4F**(P06B):維持以星期縮寫篩選 [TruckRepository.findByShopIdAndStoreIdAndDayOfWeek];無命中時再退回同樓層最早出發。 */ private fun resolveTruckForShopFloorAndDay( diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt new file mode 100644 index 0000000..a6bc9f0 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoFloorSupplierSettingsService.kt @@ -0,0 +1,95 @@ +package com.ffii.fpsms.modules.deliveryOrder.service + +import com.ffii.fpsms.modules.settings.entity.SettingsRepository +import org.springframework.stereotype.Service +import java.util.Locale + +/** 供 DO 搜尋/車線/報表 SQL 等共用的 2F/4F 供應商代碼(來自 `settings` CSV)。 */ +@Service +open class DoFloorSupplierSettingsService( + private val settingsRepository: SettingsRepository, +) { + companion object { + private const val SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F" + private const val SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F" + + private val DEFAULT_SUPPLIERS_2F = listOf("P07", "P06D", "P06Y") + private val DEFAULT_SUPPLIERS_4F = listOf("P06B") + } + + open fun supplierCodesFromSetting(settingName: String, defaultList: List): List { + val raw = settingsRepository.findByName(settingName).map { it.value }.orElse(null) + ?.trim() + .orEmpty() + if (raw.isEmpty()) return defaultList + val parsed = raw.split(",").map { it.trim() }.filter { it.isNotEmpty() }.distinct() + return parsed.ifEmpty { defaultList } + } + + open fun loadDoFloorSupplierLists(): Pair, List> { + val suppliers2F = supplierCodesFromSetting(SETTING_DO_FLOOR_SUPPLIERS_2F, DEFAULT_SUPPLIERS_2F) + val suppliers4F = supplierCodesFromSetting(SETTING_DO_FLOOR_SUPPLIERS_4F, DEFAULT_SUPPLIERS_4F) + return suppliers2F to suppliers4F + } + + open fun allowedSupplierCodesForFloor(floor: String?): List { + val f = floor?.trim()?.uppercase(Locale.ROOT).orEmpty() + val (codes2F, codes4F) = loadDoFloorSupplierLists() + return when { + f.isEmpty() || f == "ALL" || f == "All" -> (codes2F + codes4F).distinct() + f == "2F" -> codes2F + f == "4F" -> codes4F + else -> (codes2F + codes4F).distinct() + } + } + + /** 4F 清單優先;其餘預設 2F(與既有 DO 車線邏輯一致)。 */ + open fun preferredStoreFloorForSupplier( + supplierCode: String?, + suppliers2F: List, + suppliers4F: List, + ): String { + val code = supplierCode?.trim().orEmpty() + if (code.isEmpty()) return "2F" + if (suppliers4F.contains(code)) return "4F" + if (suppliers2F.contains(code)) return "2F" + return "2F" + } + + /** DO 揀貨建議:名單外供應商不限制 2F/4F。 */ + open fun preferredFloorForPickLotOrNull( + supplierCode: String?, + suppliers2F: List, + suppliers4F: List, + ): String? { + val code = supplierCode?.trim().orEmpty() + if (code.isEmpty()) return null + if (suppliers4F.contains(code)) return "4F" + if (suppliers2F.contains(code)) return "2F" + return null + } + + data class SqlPreferredFloorCases( + /** 例如 `CASE WHEN s.code IN (...) THEN '4F' ... END`(單行,可嵌入原生 SQL) */ + val floorStringCase: String, + val storeIdNumericCase: String, + ) + + /** + * 依目前 settings 產生原生 SQL CASE(供 JDBC 字串拼接)。 + * @param codeExpr 已加別名的欄位,如 `s.code`、`supplier.code` + */ + open fun sqlPreferredFloorCases(codeExpr: String = "s.code"): SqlPreferredFloorCases { + val (s2f, s4f) = loadDoFloorSupplierLists() + val in4 = joinSqlInList(s4f) + val in2 = joinSqlInList(s2f) + val floor = + "CASE WHEN $codeExpr IN ($in4) THEN '4F' WHEN $codeExpr IN ($in2) THEN '2F' ELSE NULL END" + val storeId = + "CASE WHEN $codeExpr IN ($in4) THEN 4 WHEN $codeExpr IN ($in2) THEN 2 ELSE NULL END" + return SqlPreferredFloorCases(floorStringCase = floor, storeIdNumericCase = storeId) + } + + private fun joinSqlInList(codes: List): String = + codes.joinToString(", ") { "'" + it.replace("'", "''") + "'" } +} 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 e31662a..7b3ca0a 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 @@ -103,6 +103,7 @@ class DoReleaseCoordinatorService( private val userRepository: UserRepository, private val pickOrderRepository: PickOrderRepository, private val doPickOrderRecordRepository: DoPickOrderRecordRepository, + private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, ) { private val poolSize = Runtime.getRuntime().availableProcessors() private val executor = Executors.newFixedThreadPool(min(poolSize, 4)) @@ -140,22 +141,15 @@ class DoReleaseCoordinatorService( private fun updateBatchTicketNumbers() { try { val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate") + val pfCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("s.code") val updateSql = """ UPDATE fpsmsdb.do_pick_order dpo INNER JOIN ( WITH PreferredFloor AS ( SELECT do.id AS deliveryOrderId, - CASE - WHEN s.code = 'P06B' THEN '4F' - WHEN s.code = 'P07' OR s.code = 'P06D' THEN '2F' - ELSE NULL - END AS preferred_floor, - CASE - WHEN s.code = 'P06B' THEN 4 - WHEN s.code = 'P07' OR s.code = 'P06D' THEN 2 - ELSE NULL - END AS preferred_store_id + ${pfCases.floorStringCase} AS preferred_floor, + ${pfCases.storeIdNumericCase} AS preferred_store_id FROM fpsmsdb.delivery_order do LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0 WHERE do.deleted = 0 @@ -307,20 +301,13 @@ class DoReleaseCoordinatorService( println(" DEBUG: Getting ordered IDs for ${ids.size} orders") println(" DEBUG: First 5 IDs: ${ids.take(5)}") val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate") + val pfCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("s.code") val sql = """ WITH PreferredFloor AS ( SELECT do.id AS deliveryOrderId, - CASE - WHEN s.code = 'P06B' THEN '4F' - WHEN s.code = 'P07' OR s.code = 'P06D' THEN '2F' - ELSE NULL - END AS preferred_floor, - CASE - WHEN s.code = 'P06B' THEN 4 - WHEN s.code = 'P07' OR s.code = 'P06D' THEN 2 - ELSE NULL - END AS preferred_store_id + ${pfCases.floorStringCase} AS preferred_floor, + ${pfCases.storeIdNumericCase} AS preferred_store_id FROM fpsmsdb.delivery_order do LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0 WHERE do.id IN (${ids.joinToString(",")}) diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchDopoAssignmentService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchDopoAssignmentService.kt index 77eec8e..25ef935 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchDopoAssignmentService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchDopoAssignmentService.kt @@ -144,6 +144,9 @@ open class DoWorkbenchDopoAssignmentService( sql.append(" AND dop.loadingSequence = :loadingSequence ") params["loadingSequence"] = request.loadingSequence } + if (isisExtraReleaseType(request.releaseType)) { + sql.append(" AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' ") + } // Fetch a batch of candidates and try atomic-assign sequentially. // This avoids forcing the frontend to refresh when a single picked candidate is concurrently assigned. val candidateLimit = 50 @@ -247,6 +250,9 @@ open class DoWorkbenchDopoAssignmentService( sql.append(" AND dop.loadingSequence = :loadingSequence ") params["loadingSequence"] = request.loadingSequence } + if (isisExtraReleaseType(request.releaseType)) { + sql.append(" AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' ") + } val shouldOrderBySequenceV1 = actualStoreId == "2/F" && request.loadingSequence == null if (shouldOrderBySequenceV1) { sql.append(" ORDER BY dop.requiredDeliveryDate ASC, dop.truckDepartureTime ASC, dop.loadingSequence ASC, dop.id ASC LIMIT 1 ") @@ -301,6 +307,11 @@ open class DoWorkbenchDopoAssignmentService( } else null } + private fun isisExtraReleaseType(releaseType: String?): Boolean { + val n = releaseType?.trim()?.lowercase().orEmpty() + return n == "isExtra" + } + private fun parseDepartureTimeToSql(raw: String?): Time? { if (raw.isNullOrBlank()) return null val s = raw.trim() 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 e0da6d2..f938588 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 @@ -1,3 +1,4 @@ + package com.ffii.fpsms.modules.deliveryOrder.service import com.ffii.core.support.JdbcDao @@ -54,6 +55,7 @@ import java.time.format.DateTimeFormatter import com.ffii.fpsms.modules.deliveryOrder.web.models.StoreLaneSummary import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneRow import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneBtn +import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchEtraShopLaneGroup import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleasedDoPickOrderListItem import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchTicketReleaseTableResponse import com.ffii.fpsms.modules.user.service.UserService @@ -670,6 +672,7 @@ return MessageResponse( val releaseFilterClause = when (rt) { "batch" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'batch' " "single" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'single' " + "isExtra" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' " else -> "" } val sql = """ @@ -812,6 +815,7 @@ return MessageResponse( unassigned = it.unassigned, total = it.total, handlerName = it.handlerName, + storeId = actualStoreId, ) } .sortedWith( @@ -853,24 +857,181 @@ return MessageResponse( ) } + /** + * Workbench Etra view: all `delivery_order_pick_order` with `releaseType` = isExtra (case-insensitive), + * for one [requiredDeliveryDate], grouped by shop then by truck / time / loading sequence. + */ + open fun getWorkbenchEtraLaneSummary(requiredDate: LocalDate?): List { + val targetDate = requiredDate ?: LocalDate.now() + val defaultTruck = truckRepository.findById(5577L).orElse(null) + val defaultTruckLaneCode = defaultTruck?.truckLanceCode ?: "" + + val sql = """ + SELECT + dop.shopCode AS shopCode, + dop.shopName AS shopName, + dop.storeId AS storeId, + dop.truckDepartureTime AS truckDepartureTime, + dop.truckLanceCode AS truckLanceCode, + dop.loadingSequence AS loadingSequence, + COUNT(DISTINCT dop.id) AS total_cnt, + SUM(CASE WHEN dop.handledBy IS NULL THEN 1 ELSE 0 END) AS unassigned_cnt, + GROUP_CONCAT( + DISTINCT NULLIF(TRIM(dop.handlerName), '') + ORDER BY dop.handlerName + SEPARATOR ', ' + ) AS handler_names + FROM fpsmsdb.delivery_order_pick_order dop + WHERE dop.deleted = 0 + AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' + AND dop.requiredDeliveryDate = :requiredDate + AND dop.ticketStatus IN ('pending', 'released') + AND EXISTS ( + SELECT 1 FROM fpsmsdb.pick_order po + WHERE po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0 + ) + GROUP BY dop.shopCode, dop.shopName, dop.storeId, dop.truckDepartureTime, dop.truckLanceCode, dop.loadingSequence + """.trimIndent() + + val rawRows: List> = try { + jdbcDao.queryForList(sql, mapOf("requiredDate" to targetDate)) + } catch (e: Exception) { + println("❌ getWorkbenchEtraLaneSummary: ${e.message}") + emptyList() + } + + fun cellStr(row: Map, name: String): String? { + val k = row.keys.find { it.equals(name, true) } ?: return null + return row[k]?.toString()?.trim()?.takeIf { it.isNotEmpty() } + } + fun cellNum(row: Map, vararg names: String): Int { + for (n in names) { + val k = row.keys.find { it.equals(n, true) } ?: continue + (row[k] as? Number)?.toInt()?.let { return it } + } + return 0 + } + fun cellNullableInt(row: Map, vararg names: String): Int? { + for (n in names) { + val k = row.keys.find { it.equals(n, true) } ?: continue + val v = row[k] ?: continue + when (v) { + is Number -> return v.toInt() + is String -> v.trim().toIntOrNull()?.let { return it } + } + } + return null + } + + data class EtraAgg( + val shopCode: String?, + val shopName: String?, + val storeId: String?, + val sortTime: LocalTime, + val lance: String, + val loadingSequence: Int?, + val unassigned: Int, + val total: Int, + val handlerName: String?, + ) + + val aggs = rawRows.mapNotNull { row -> + val lance = cellStr(row, "truckLanceCode") ?: return@mapNotNull null + if (lance == defaultTruckLaneCode) return@mapNotNull null + val storeIdCol = cellStr(row, "storeId") + val ttKey = row.keys.find { it.equals("truckDepartureTime", true) } + val ttVal = ttKey?.let { row[it] } + val sortTime = when (ttVal) { + null -> LocalTime.MIDNIGHT + is java.sql.Time -> ttVal.toLocalTime() + is LocalTime -> ttVal + is java.sql.Timestamp -> ttVal.toLocalDateTime().toLocalTime() + else -> runCatching { LocalTime.parse(ttVal.toString().take(8)) }.getOrNull() + ?: runCatching { LocalTime.parse(ttVal.toString()) }.getOrNull() + ?: LocalTime.MIDNIGHT + } + val loadingSeq = cellNullableInt(row, "loadingSequence") + val unassigned = cellNum(row, "unassigned_cnt", "unassignedCnt") + val total = cellNum(row, "total_cnt", "totalCnt") + if (total <= 0) return@mapNotNull null + EtraAgg( + shopCode = cellStr(row, "shopCode"), + shopName = cellStr(row, "shopName"), + storeId = storeIdCol, + sortTime = sortTime, + lance = lance, + loadingSequence = loadingSeq, + unassigned = unassigned, + total = total, + handlerName = cellStr(row, "handler_names"), + ) + } + + val byShop = aggs.groupBy { a -> + listOf(a.shopCode ?: "", a.shopName ?: "").joinToString("|") + } + + return byShop.entries + .map { (key, group) -> + val head = group.first() + val lanes = group + .sortedWith( + compareBy { it.sortTime } + .thenBy { it.lance } + .thenBy { it.loadingSequence ?: 999 } + ) + .map { + val is4F = it.storeId?.replace("/", "")?.trim()?.equals("4F", ignoreCase = true) == true + LaneBtn( + truckLanceCode = it.lance, + loadingSequence = if (is4F) it.loadingSequence else null, + unassigned = it.unassigned, + total = it.total, + handlerName = it.handlerName, + storeId = it.storeId, + truckDepartureTime = it.sortTime.toString(), + ) + } + WorkbenchEtraShopLaneGroup( + shopCode = head.shopCode, + shopName = head.shopName, + lanes = lanes, + ) + } + .sortedWith( + compareBy { it.shopName ?: it.shopCode ?: "" } + .thenBy { it.shopCode ?: "" } + ) + } + open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelection( shopName: String?, storeId: String?, truck: String?, + releaseTypeFilter: String? = null, ): List = - queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = true) + queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = true, releaseTypeFilter = releaseTypeFilter) /** * @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. */ open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( shopName: String?, storeId: String?, truck: String?, requiredDeliveryDate: LocalDate? = null, + releaseTypeFilter: String? = null, ): List = - queryWorkbenchReleasedDopoList(shopName, storeId, truck, beforeToday = false, equalsDeliveryDate = requiredDeliveryDate) + queryWorkbenchReleasedDopoList( + shopName, + storeId, + truck, + beforeToday = false, + equalsDeliveryDate = requiredDeliveryDate, + releaseTypeFilter = releaseTypeFilter, + ) /** * Workbench completed tickets: query `delivery_order_pick_order` where `ticketStatus = completed`. @@ -1362,8 +1523,9 @@ return MessageResponse( dop.deliveryNoteCode = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode) } deliveryOrderPickOrderRepository.save(dop) + } - + markDeliveryOrdersCompletedForDeliveryOrderPickOrder(deliveryOrderPickOrderId) return MessageResponse( id = dop.id, code = "SUCCESS", @@ -1468,6 +1630,7 @@ return MessageResponse( truck: String?, beforeToday: Boolean, equalsDeliveryDate: LocalDate? = null, + releaseTypeFilter: String? = null, ): List { val today = LocalDate.now() val params = mutableMapOf() @@ -1518,6 +1681,10 @@ return MessageResponse( sqlBuilder.append(" AND (dop.shopName LIKE :shopPat OR dop.shopCode LIKE :shopPat) ") params["shopPat"] = "%${shopName.trim()}%" } + val rtNorm = releaseTypeFilter?.trim()?.lowercase().orEmpty() + if (rtNorm == "isExtra") { + sqlBuilder.append(" AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' ") + } sqlBuilder.append(" ORDER BY dop.requiredDeliveryDate, dop.truckDepartureTime, dop.truckLanceCode, dop.id ") val rows: List> = try { jdbcDao.queryForList(sqlBuilder.toString(), params) @@ -1912,6 +2079,7 @@ return MessageResponse( tryCompleteDeliveryOrderPickOrderTicketCompleted(poId) } } + private fun registerAfterCommit(action: () -> Unit) { if (!TransactionSynchronizationManager.isSynchronizationActive()) { action() @@ -2047,6 +2215,7 @@ return MessageResponse( ) } } + private fun postWorkbenchPickSideEffects(savedStockOutLine: StockOutLine, deltaQty: BigDecimal, createLedger: Boolean = true) { if (deltaQty <= BigDecimal.ZERO) return val wall0 = System.nanoTime() @@ -2229,9 +2398,10 @@ return MessageResponse( throw last ?: RuntimeException("Failed to complete pick order after retries (poId=$poId)") } - /** + /** * Workbench completion: if all pick_orders under the same delivery_order_pick_order are completed, - * update ONLY delivery_order_pick_order.ticketStatus (no do_pick_order/do_pick_order_line records). + * update delivery_order_pick_order.ticketStatus and related delivery_order.status → completed. + * Does not create do_pick_order / do_pick_order_line records. */ private fun tryCompleteDeliveryOrderPickOrderTicketCompleted(poId: Long) { val dopRow = jdbcDao.queryForMap( @@ -2276,8 +2446,36 @@ return MessageResponse( """.trimIndent(), mapOf("dopId" to dopId, "deliveryNoteCode" to newDeliveryNoteCode), ) + markDeliveryOrdersCompletedForDeliveryOrderPickOrder(dopId) + } + /** + * When a workbench ticket ([delivery_order_pick_order]) is completed, align linked [delivery_order] headers. + */ + private fun markDeliveryOrdersCompletedForDeliveryOrderPickOrder(dopId: Long) { + if (dopId <= 0L) return + val rows = try { + jdbcDao.queryForList( + """ + SELECT DISTINCT po.doId AS doId + FROM fpsmsdb.pick_order po + WHERE po.deliveryOrderPickOrderId = :dopId + AND po.deleted = 0 + AND po.doId IS NOT NULL + """.trimIndent(), + mapOf("dopId" to dopId), + ) + } catch (_: Exception) { + emptyList() + } + for (row in rows) { + val doId = (row["doId"] as? Number)?.toLong() ?: continue + val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId) ?: continue + if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED) continue + deliveryOrder.status = DeliveryOrderStatus.COMPLETED + deliveryOrder.completeDate = LocalDateTime.now() + deliveryOrderRepository.save(deliveryOrder) + } } - private fun checkWorkbenchPickOrderLineCompleted(pickOrderLineId: Long, allStockOutLines: List) { val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return if (pol.status == PickOrderLineStatus.COMPLETED) return @@ -2715,11 +2913,7 @@ return MessageResponse( } } - /** - * Carton label reprint for workbench: [request.doPickOrderId] is [delivery_order_pick_order.id], - * same as [getWorkbenchPrintContext]. Legacy [DeliveryOrderService.printDNLabelsReprint] expects - * [do_pick_order_record.recordId] and must not be used here. - */ + private fun exportDNLabelsReprintWorkbench(request: PrintDNLabelsReprintRequest): Map { validateWorkbenchCartonReprintRange( fromCarton = request.fromCarton, 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 c138a85..5307654 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 @@ -359,14 +359,17 @@ open class DoWorkbenchReleaseService( } /** - * `TI-B-yyyyMMdd-2F-001` (batch) or `TI-S-yyyyMMdd-2F-001` (single), same suffix rules as [DoReleaseCoordinatorService] / legacy `do_pick_order`. + * `TI-B-yyyyMMdd-2F-001` (batch), `TI-S-yyyyMMdd-2F-001` (single), or `TI-E-yyyyMMdd-2F-001` (Etra), + * same suffix rules as [DoReleaseCoordinatorService] / legacy `do_pick_order`. */ private fun nextDeliveryOrderPickOrderTicketNo( requiredDate: LocalDate, storeDisplay: String, ticketLetter: String, ): String { - require(ticketLetter == "B" || ticketLetter == "S") { "ticketLetter must be B or S" } + require(ticketLetter == "B" || ticketLetter == "S" || ticketLetter == "E") { + "ticketLetter must be B, S or E" + } val ymd = requiredDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) val floor = storeDisplay.replace("/", "").trim() val prefix = "TI-$ticketLetter-$ymd-$floor-" @@ -397,6 +400,9 @@ 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,11 +446,6 @@ open class DoWorkbenchReleaseService( ): Int { if (results.isEmpty()) return 0 - val releaseTypeCol = when (dopReleaseType.lowercase()) { - "single" -> "single" - else -> "batch" - } - val grouped = results.groupBy { listOf( it.shopId?.toString() ?: "", @@ -452,7 +453,8 @@ open class DoWorkbenchReleaseService( it.preferredFloor, it.truckId?.toString() ?: "", it.truckDepartureTime?.toString() ?: "", - it.truckLanceCode ?: "" + it.truckLanceCode ?: "", + it.isExtra.toString(), ).joinToString("|") } @@ -477,7 +479,16 @@ open class DoWorkbenchReleaseService( (storeId ?: "2/F").replace("/", "").trim() } val requiredDate = first.estimatedArrivalDate ?: LocalDate.now() - val tempTicket = if (releaseTypeCol == "single") { + val releaseTypeCol = if (first.isExtra) { + "isExtra" + } else if (dopReleaseType.equals("single", ignoreCase = true)) { + "single" + } else { + "batch" + } + val tempTicket = if (first.isExtra) { + nextDeliveryOrderPickOrderEtraTicketNo(requiredDate, ticketFloorSegment) + } else if (releaseTypeCol == "single") { nextDeliveryOrderPickOrderSingleTicketNo(requiredDate, ticketFloorSegment) } else { nextDeliveryOrderPickOrderBatchTicketNo(requiredDate, ticketFloorSegment) diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt index 8836198..f803111 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt @@ -72,7 +72,7 @@ class DeliveryOrderController( pageSize = request.pageSize, truckLanceCode = request.truckLanceCode, floor = request.floor, - isEtra = request.isEtra, + isExtra = request.isExtra, ) } @@ -89,7 +89,7 @@ class DeliveryOrderController( pageNum = request.pageNum, pageSize = request.pageSize, floor = request.floor, - isEtra = request.isEtra, + isExtra = request.isExtra, ) } @@ -108,7 +108,7 @@ class DeliveryOrderController( pageSize = request.pageSize, truckLanceCode = request.truckLanceCode, floor = request.floor, - isEtra = request.isEtra, + isExtra = request.isExtra, ) } 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 ebd6dab..ffcdcae 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 @@ -96,14 +96,27 @@ class DoWorkbenchController( ) } + /** All Etra workbench tickets for a day, grouped by shop → truck (see [DoWorkbenchMainService.getWorkbenchEtraLaneSummary]). */ + @GetMapping("/summary-is-etra") + fun getWorkbenchEtraSummary( + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?, + ): List = + doWorkbenchMainService.getWorkbenchEtraLaneSummary(requiredDate) + /** Past-date backlog tickets from `delivery_order_pick_order` (not `do_pick_order`). */ @GetMapping("/released") fun getWorkbenchReleasedDoPickOrders( @RequestParam(required = false) shopName: String?, @RequestParam(required = false) storeId: String?, - @RequestParam(required = false) truck: String? + @RequestParam(required = false) truck: String?, + @RequestParam(required = false) releaseType: String?, ): List { - return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelection(shopName, storeId, truck) + return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelection( + shopName, + storeId, + truck, + releaseTypeFilter = releaseType, + ) } @GetMapping("/released-today") @@ -112,12 +125,14 @@ class DoWorkbenchController( @RequestParam(required = false) storeId: String?, @RequestParam(required = false) truck: String?, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?, + @RequestParam(required = false) releaseType: String?, ): List { return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( shopName, storeId, truck, requiredDeliveryDate = requiredDate, + releaseTypeFilter = releaseType, ) } diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt index 4643119..141665d 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt @@ -19,7 +19,7 @@ data class DoDetailResponse( val completeDate: LocalDateTime?, val status: String?, /** 加單 DO(M18 加單專用同步) */ - val isEtra: Boolean = false, + val isExtra: Boolean = false, val deliveryOrderLines: List ) @@ -51,7 +51,18 @@ data class LaneBtn( val unassigned: Int, val total: Int, // 同一 truckLanceCode + loadingSequence 的 handler 去重后逗号拼接 - val handlerName: String? = null + val handlerName: String? = null, + /** Workbench Etra lane: `delivery_order_pick_order.storeId` (2/F, 4/F, …) for assign / modal scope */ + val storeId: String? = null, + /** Workbench Etra / lane row: `truckDepartureTime` as ISO local time string for assign-by-lane */ + val truckDepartureTime: String? = null, +) + +/** All Etra (`releaseType=isExtra`) tickets for a day, grouped by shop then truck (no 2F/4F split in UI). */ +data class WorkbenchEtraShopLaneGroup( + val shopCode: String?, + val shopName: String?, + val lanes: List, ) data class AssignByLaneRequest( val userId: Long, @@ -59,7 +70,9 @@ data class AssignByLaneRequest( val truckDepartureTime: String?, // 可选:限定出车时间 val truckLanceCode: String , val loadingSequence: Int? = null, - val requiredDate: LocalDate? // 必填:车道编号 + val requiredDate: LocalDate?, // 必填:车道编号 + /** When `isExtra`, assignment candidates are limited to `releaseType = isExtra` rows. */ + val releaseType: String? = null, ) data class DoPickOrderSummaryItem( val truckDepartureTime: java.time.LocalTime?, diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt index 8ecd928..5c827a8 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt @@ -21,7 +21,8 @@ data class ReleaseDoResult( val truckDepartureTime: LocalTime?, val truckLanceCode: String?, - val loadingSequence: Int? + val loadingSequence: Int?, + val isExtra: Boolean = false, ) data class SearchDeliveryOrderInfoRequest( val code: String?, @@ -31,8 +32,8 @@ data class SearchDeliveryOrderInfoRequest( val pageSize: Int?, val pageNum: Int?, val truckLanceCode: String?, - /** `ALL`/`All`/null:P06B+P07+P06D;`2F`:P07+P06D;`4F`:P06B。車線-X 亦依供應商歸屬出現在對應樓層。 */ + /** `ALL`/`All`/null:P06B+P07+P06D+P06Y;`2F`:P07+P06D+P06Y ;`4F`:P06B。車線-X 亦依供應商歸屬出現在對應樓層。 */ val floor: String? = null, - /** null:不篩 isEtra;true/false:只顯示加單或非加單 DO */ - val isEtra: Boolean? = null, + /** null:不篩 isExtra;true/false:只顯示加單或非加單 DO */ + val isExtra: Boolean? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/SaveDeliveryOrderRequest.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/SaveDeliveryOrderRequest.kt index bc89a77..6a1bcda 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/SaveDeliveryOrderRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/SaveDeliveryOrderRequest.kt @@ -20,7 +20,7 @@ data class SaveDeliveryOrderRequest( val handlerId: Long?, val m18BeId: Long?, val deleted: Boolean? = false, - val isEtra: Boolean? = false, + val isExtra: Boolean? = false, ) data class SaveDeliveryOrderStatusRequest( diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt index 5eb8f01..170fff1 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/HierarchicalFgPayloadAssembler.kt @@ -240,6 +240,9 @@ ORDER BY "id" to row["stockOutLineId"], "status" to row["stockOutLineStatus"], "qty" to row["stockOutLineQty"], + "requiredQty" to row["requiredQty"], + "suggestedPickLotQty" to row["requiredQty"], + "suggestedPickLotId" to row["suggestedPickLotId"], "lotId" to lotId, "lotNo" to (row["lotNo"] ?: ""), "location" to (row["location"] ?: ""), diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index 67fec79..f6c5bf6 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -10,6 +10,7 @@ import com.ffii.fpsms.m18.M18GrnRules import com.ffii.fpsms.modules.master.entity.ShopRepository import com.ffii.fpsms.modules.master.enums.ShopType import com.ffii.fpsms.modules.master.service.ItemUomService +import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService import java.math.BigDecimal import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter import net.sf.jasperreports.export.SimpleExporterInput @@ -20,6 +21,7 @@ open class ReportService( private val jdbcDao: JdbcDao, private val itemUomService: ItemUomService, private val shopRepository: ShopRepository, + private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, ) { /** * Queries the database for inventory data based on dates and optional item type. @@ -118,6 +120,8 @@ open class ReportService( "AND DATE(IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)" } else "" + val supplierFloorSqlCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("supplier.code") + val sql = """ SELECT IFNULL(DATE_FORMAT( @@ -143,17 +147,9 @@ FORMAT(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 0), 0) AS qty, FROM truck t2 WHERE t2.shopId = do.shopId AND t2.deleted = 0 - AND t2.Store_id = CASE - WHEN supplier.code = 'P06B' THEN '4F' - WHEN supplier.code IN ('P07', 'P06D') THEN '2F' - ELSE NULL - END + AND t2.Store_id = ${supplierFloorSqlCases.floorStringCase} AND ( - (CASE - WHEN supplier.code = 'P06B' THEN '4F' - WHEN supplier.code IN ('P07', 'P06D') THEN '2F' - ELSE NULL - END + (${supplierFloorSqlCases.floorStringCase} AND (SELECT COUNT(*) FROM truck t3 WHERE t3.shopId = do.shopId AND t3.deleted = 0 AND t3.Store_id = '4F') > 1 @@ -170,11 +166,7 @@ FORMAT(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 0), 0) AS qty, ELSE '' END, '%')) OR - t2.Store_id = CASE - WHEN supplier.code = 'P06B' THEN '4F' - WHEN supplier.code IN ('P07', 'P06D') THEN '2F' - ELSE NULL - END + t2.Store_id = ${supplierFloorSqlCases.floorStringCase} ) ORDER BY t2.DepartureTime ASC LIMIT 1), diff --git a/src/main/java/com/ffii/fpsms/modules/settings/web/SettingsController.java b/src/main/java/com/ffii/fpsms/modules/settings/web/SettingsController.java index 046037f..acf0d79 100644 --- a/src/main/java/com/ffii/fpsms/modules/settings/web/SettingsController.java +++ b/src/main/java/com/ffii/fpsms/modules/settings/web/SettingsController.java @@ -5,6 +5,7 @@ import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -41,13 +42,24 @@ public class SettingsController{ // @PreAuthorize("hasAuthority('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) public void update(@PathVariable String name, @RequestBody @Valid UpdateReq body) { + applyUpdate(name, body); + } + + /** Same as PATCH; use from browsers where CORS preflight for PATCH is blocked. */ + @PostMapping("/{name}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updatePost(@PathVariable String name, @RequestBody @Valid UpdateReq body) { + applyUpdate(name, body); + } + + private void applyUpdate(String name, UpdateReq body) { Settings entity = this.settingsService.findByName(name) .orElseThrow(NotFoundException::new); - if (!this.settingsService.validateType(entity.getType(), body.value)) { + if (!this.settingsService.validateType(entity.getType(), body.getValue())) { throw new BadRequestException(); } - entity.setValue(body.value); + entity.setValue(body.getValue()); this.settingsService.save(entity); } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index 6060b86..48692d9 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt @@ -42,6 +42,7 @@ import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository import com.ffii.fpsms.modules.stock.web.model.StockOutStatus import com.ffii.fpsms.modules.common.SecurityUtils +import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService @Service open class SuggestedPickLotService( val suggestedPickLotRepository: SuggestPickLotRepository, @@ -57,7 +58,8 @@ open class SuggestedPickLotService( val failInventoryLotLineRepository: FailInventoryLotLineRepository, val stockOutRepository: StockOutRepository, val itemRepository: ItemsRepository, - val stockOutLineRepository: StockOutLIneRepository + val stockOutLineRepository: StockOutLIneRepository, + private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, ) { // Calculation Available Qty / Remaining Qty @@ -114,6 +116,8 @@ open class SuggestedPickLotService( .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today)} .sortedBy { it.expiryDate } .groupBy { it.item?.id } + + val (floorSuppliers2F, floorSuppliers4F) = doFloorSupplierSettingsService.loadDoFloorSupplierLists() // loop for suggest pick lot line pols.forEach { line -> @@ -126,11 +130,11 @@ open class SuggestedPickLotService( val doPreferredFloor: String? = if (isDoPickOrder) { val supplierCode = pickOrder?.deliveryOrder?.supplier?.code - when (supplierCode) { - "P06B" -> "4F" - "P07", "P06D" -> "2F" - else -> null // 其他供应商不限定 2F/4F - } + doFloorSupplierSettingsService.preferredFloorForPickLotOrNull( + supplierCode, + floorSuppliers2F, + floorSuppliers4F, + ) } else { null } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt new file mode 100644 index 0000000..3ba0c02 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTakeForSectionsRequest.kt @@ -0,0 +1,8 @@ +package com.ffii.fpsms.modules.stock.web.model + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class CreateStockTakeForSectionsRequest( + val sections: List? = null, +) diff --git a/src/main/resources/db/changelog/changes/20260514_Enson/01_setting.sql b/src/main/resources/db/changelog/changes/20260514_Enson/01_setting.sql new file mode 100644 index 0000000..465fea8 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260514_Enson/01_setting.sql @@ -0,0 +1,18 @@ +--liquibase formatted sql + +-- DO 樓層供應商代碼(逗號分隔),name 須與前端 constants 一致。預設值對齊既有硬編碼邏輯,後端改讀 settings 後才會生效。 +--changeset Enson:20260514-01 +INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`) +SELECT 'DO.floor.suppliers.2F', 'P07,P06D,P06Y', 'DO_FLOOR', 'string' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'DO.floor.suppliers.2F' +); + +--changeset Enson:20260514-02 +INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`) +SELECT 'DO.floor.suppliers.4F', 'P06B', 'DO_FLOOR', 'string' +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'DO.floor.suppliers.4F' +); diff --git a/src/main/resources/db/changelog/changes/20260514_Enson/02_setting.sql b/src/main/resources/db/changelog/changes/20260514_Enson/02_setting.sql new file mode 100644 index 0000000..e39fece --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260514_Enson/02_setting.sql @@ -0,0 +1,6 @@ +--liquibase formatted sql + +-- 修改 delivery_order 表的 isExtra 欄位為 isExtra +--changeset Enson:20260514-03 +ALTER TABLE `delivery_order` CHANGE COLUMN `isEtra` `isExtra` TINYINT(1) NOT NULL DEFAULT 0; +