isEtra new do chart do saerch batch release button put down not lot requied qty show 0 fixproduction
| @@ -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 = | |||
| @@ -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") | |||
| @@ -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<String>? | |||
| staffNos: List<String>?, | |||
| storeId: String?, | |||
| storeIdNull: Boolean?, | |||
| ): List<Map<String, Any>> { | |||
| val args = mutableMapOf<String, Any>() | |||
| 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) | |||
| @@ -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<String>?, | |||
| @RequestParam(required = false) storeId: String?, | |||
| @RequestParam(required = false) storeIdNull: Boolean?, | |||
| ): List<Map<String, Any>> = | |||
| chartService.getStaffDeliveryPerformance(startDate, endDate, staffNo) | |||
| chartService.getStaffDeliveryPerformance(startDate, endDate, staffNo, storeId, storeIdNull) | |||
| // ---------- Job order reports ---------- | |||
| @@ -64,6 +64,6 @@ open class DeliveryOrder: BaseEntity<Long>() { | |||
| 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 | |||
| } | |||
| @@ -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<DeliveryOrderInfoLite> | |||
| @@ -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<String>, | |||
| pageable: Pageable, | |||
| ): Page<DeliveryOrderInfoLite> | |||
| @@ -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, | |||
| ) | |||
| @@ -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<String> { | |||
| 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<String> = | |||
| doFloorSupplierSettingsService.allowedSupplierCodesForFloor(floor) | |||
| private fun loadDoFloorSupplierLists(): Pair<List<String>, List<String>> = | |||
| doFloorSupplierSettingsService.loadDoFloorSupplierLists() | |||
| private fun preferredStoreFloorForSupplier( | |||
| supplierCode: String?, | |||
| suppliers2F: List<String>, | |||
| suppliers4F: List<String>, | |||
| ): 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<DeliveryOrderInfoLiteDto> { | |||
| 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<DeliveryOrderInfoLiteDto> { | |||
| 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<DeliveryOrderInfoLiteDto> { | |||
| 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<String>, | |||
| lanePredicate: (String?) -> Boolean, | |||
| ): List<DeliveryOrderInfoLiteDto> { | |||
| @@ -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( | |||
| @@ -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<String>): List<String> { | |||
| 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<String>, List<String>> { | |||
| 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<String> { | |||
| 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<String>, | |||
| suppliers4F: List<String>, | |||
| ): 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<String>, | |||
| suppliers4F: List<String>, | |||
| ): 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>): String = | |||
| codes.joinToString(", ") { "'" + it.replace("'", "''") + "'" } | |||
| } | |||
| @@ -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(",")}) | |||
| @@ -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() | |||
| @@ -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<WorkbenchEtraShopLaneGroup> { | |||
| 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<Map<String, Any?>> = try { | |||
| jdbcDao.queryForList(sql, mapOf("requiredDate" to targetDate)) | |||
| } catch (e: Exception) { | |||
| println("❌ getWorkbenchEtraLaneSummary: ${e.message}") | |||
| emptyList() | |||
| } | |||
| fun cellStr(row: Map<String, Any?>, 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<String, Any?>, 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<String, Any?>, 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<EtraAgg> { 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<WorkbenchEtraShopLaneGroup> { it.shopName ?: it.shopCode ?: "" } | |||
| .thenBy { it.shopCode ?: "" } | |||
| ) | |||
| } | |||
| open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelection( | |||
| shopName: String?, | |||
| storeId: String?, | |||
| truck: String?, | |||
| releaseTypeFilter: String? = null, | |||
| ): List<ReleasedDoPickOrderListItem> = | |||
| 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<ReleasedDoPickOrderListItem> = | |||
| 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<ReleasedDoPickOrderListItem> { | |||
| val today = LocalDate.now() | |||
| val params = mutableMapOf<String, Any>() | |||
| @@ -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<Map<String, Any?>> = 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<StockOutLineInfo>) { | |||
| 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<String, Any> { | |||
| validateWorkbenchCartonReprintRange( | |||
| fromCarton = request.fromCarton, | |||
| @@ -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) | |||
| @@ -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, | |||
| ) | |||
| } | |||
| @@ -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<WorkbenchEtraShopLaneGroup> = | |||
| 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<ReleasedDoPickOrderListItem> { | |||
| 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<ReleasedDoPickOrderListItem> { | |||
| return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( | |||
| shopName, | |||
| storeId, | |||
| truck, | |||
| requiredDeliveryDate = requiredDate, | |||
| releaseTypeFilter = releaseType, | |||
| ) | |||
| } | |||
| @@ -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<DoDetailLineResponse> | |||
| ) | |||
| @@ -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<LaneBtn>, | |||
| ) | |||
| 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?, | |||
| @@ -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, | |||
| ) | |||
| @@ -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( | |||
| @@ -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"] ?: ""), | |||
| @@ -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), | |||
| @@ -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); | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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<String>? = null, | |||
| ) | |||
| @@ -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' | |||
| ); | |||
| @@ -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; | |||